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

Mapping Key Computation Error in scrvUSD Balance Retrieval

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

In the scrvUSD Verifier contract (V1), one of the critical parameters needed to update the cross-chain oracle is the scrvUSD contract’s own balance (i.e., “balanceOf(self)”). This value is obtained from a mapping stored in the scrvUSD contract. However, the verifier incorrectly computes the storage key for this mapping by reversing the order of parameters.

Faulty Code:
The verifier defines the storage slot for retrieving its own balance as:

uint256(keccak256(abi.encode(18, SCRVUSD)))

Here, 18 is intended to be the mapping’s slot number and SCRVUSD is the key (the contract’s address). According to Solidity’s standard, the correct storage key for a mapping is computed as:

keccak256(abi.encode(key, slot))

In this case, the correct computation should be:

uint256(keccak256(abi.encode(SCRVUSD, 18)))

By swapping the order, the verifier is looking up a completely different key in the scrvUSD contract’s storage.


How Mappings Are Stored in Ethereum

For a mapping declared in Solidity such as:

mapping(address => uint256) balances;

stored at slot p (here, p = 18), the value for a key k (in our case, SCRVUSD) is stored at:

keccak256(abi.encode(k, p))

That is, the key (k) comes first, followed by the slot (p). Any deviation from this order will yield an incorrect storage location.


In the verifierV1.sol contract, the array of storage slot identifiers is defined as:

uint256[PROOF_CNT] internal PARAM_SLOTS = [
uint256(0), // filler for account proof
uint256(21), // total_debt
uint256(22), // total_idle
uint256(20), // totalSupply
uint256(38), // full_profit_unlock_date
uint256(39), // profit_unlocking_rate
uint256(40), // last_profit_update
uint256(keccak256(abi.encode(18, SCRVUSD))) // balanceOf(self)
];

For plain variables (such as total_debt, etc.), this contract should use the direct padded representation. For a mapping entry like balanceOf(self), the key must be computed as:

keccak256(abi.encode(SCRVUSD, 18))

instead of

keccak256(abi.encode(18, SCRVUSD))

Impact

  • Incorrect Balance Extraction:
    Because the computed key does not match the actual key where the scrvUSD balance is stored, the state proof will not locate the correct balance value. In a typical proof (e.g., using eth_getProof), the balance is stored under keccak256(abi.encode(SCRVUSD, 18)). The verifier, however, will look for a storage entry under the incorrect key, leading it to return a default value (often zero).

  • Impact on Oracle Price Computation:
    The scrvUSD balance is a vital parameter in calculating the vault’s total assets and, ultimately, the price per share. An incorrect balance (e.g., zero instead of the actual balance) would lead to a miscalculation of the scrvUSD price. This can cause either:

    • Denial of Update: The oracle may reject the proof if critical parameters are missing.

    • Price Manipulation: An attacker might intentionally supply a crafted state proof for the incorrect key to force an arbitrarily low (or otherwise manipulated) balance, causing the oracle to update with erroneous data.

Severity: High.
The scrvUSD balance is integral to computing the overall vault valuation. An error here directly affects the oracle’s price, which in turn can lead to significant financial losses in any dependent pool.

  • Exploitation Complexity:
    An attacker does not need to compromise any cryptographic primitives; they simply need to provide a state proof and benefit from the verifier’s incorrect storage key lookup. Even if the honest prover supplies correct data, the verifier will still extract an incorrect value (likely 0).

  • Impact:

    • Mispricing in the oracle can lead to significant arbitrage opportunities.

    • A manipulated price could destabilize the stableswap pool, draining liquidity or causing cascading economic failures.


Attack Path

4.1. Attack Scenario

  1. State of the scrvUSD Vault:
    Assume the scrvUSD contract stores its own balance in a mapping at slot 18. The correct key for the balance of scrvUSD itself is:

    keccak256(abi.encode(SCRVUSD, 18))

    and let’s say the actual balance is a nonzero value XX.

  2. Verifier’s Incorrect Key Calculation:
    The verifier instead calculates:

    keccak256(abi.encode(18, SCRVUSD))

    This yields a different 32‑byte value that does not exist in the scrvUSD contract’s storage. Therefore, when the state proof is processed, the extracted balance value defaults to 0.

  3. Oracle Update with Incorrect Data:
    The verifier then calls the oracle’s update_price function with the array of parameters, where the balance value is now incorrect (zero). The oracle, using these parameters, computes the price per share using a formula similar to:

    price = total_assets * 1e18 / total_supply

    If the balance component is part of total_assets (or affects profit calculations), the computed price will be misrepresented.

  4. Exploitation by an Attacker:
    An attacker (or a malicious prover) could intentionally craft a state proof that supplies an artificially low balance value (or simply rely on the verifier’s error to return zero) to force the oracle into updating the scrvUSD price to a manipulated value.

    • For example, if the oracle updates the price using the faulty balance, the scrvUSD price could be set much lower than intended, allowing an attacker to buy scrvUSD at a discounted rate on a stableswap pool and then later sell it when the price corrects.


Proof of Concept (PoC)

  1. Expected Correct Proof:

    • A legitimate state proof (from an Ethereum archive node) returns the balance value XX under the key:

      bytes32 correctKey = keccak256(abi.encode(SCRVUSD, 18));
    • The verifier should extract XX if it used the correct key.

  2. Verifier’s Behavior:

    • The verifier uses:

      bytes32 faultyKey = keccak256(abi.encode(18, SCRVUSD));
    • Since the scrvUSD contract’s storage does not contain an entry under faultyKey, the RLP proof extraction returns a default value (0).

  3. Outcome:

    • The oracle receives parameters with the scrvUSD balance set to 0.

    • This leads to an incorrect price computation (for instance, if total assets or profit-related calculations rely on the balance).

  4. Demonstration:

    • An attacker could supply a valid state proof that—when processed by the verifier’s flawed logic—yields a zero balance.

    • The resulting mispricing on the target chain can be exploited via arbitrage on a stableswap pool. For example, the pool might temporarily reflect a scrvUSD price so low that a trader could buy large quantities at an underpriced rate, profiting when the price corrects.


    Recommendation


    Update the computation of the storage key for the scrvUSD balance. Change:

    uint256(keccak256(abi.encode(18, SCRVUSD)))

    to:

    uint256(keccak256(abi.encode(SCRVUSD, 18)))

    This correction ensures that the state proof correctly retrieves the actual balance stored under the proper key.

Updates

Lead Judging Commences

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

[invalid] finding-ScrvusdVerifierV1-incorrect-storage-slot-balanceOf-compute

- Per sponsor comments, verified slot is vyper, solidity contract only verifies it. - Vyper computes storage slots different from solidity as seen [here](https://ethereum.stackexchange.com/questions/149311/storage-collision-in-vyper-hashmap)

Support

FAQs

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