Within the ScrvusdVerifierV1
/ ScrvusdVerifierV2
contracts, several storage slot positions (e.g., entries in PARAM_SLOTS
and the way balanceOf(self)
is hashed) are hardcoded in an attempt to read data from a target contract (SCRVUSD
). However, when comparing these assumptions to the real storage layout of the Vyper Yearn V3 Vault (hereafter referred to as “Vault”), it becomes apparent that the verifier’s calculations do not match the Vault’s actual storage assignments. As a result, the Verifier may read incorrect storage values whenever it tries to access balanceOf(self)
or other fields. If an attacker exploits these discrepancies—or if downstream logic relies on such data without further checks—the result could be severe economic or operational risks.
balanceOf(self)
Slots Are Computed in the CodeWithin the ScrvusdVerifierV1
contract, the following crucial snippet (simplified) appears:
Notice how the last element, uint256(keccak256(abi.encode(18, SCRVUSD)))
, is assumed to represent the slot for “balanceOf(self).” During the read operation, it is also subjected to another keccak256(abi.encode(slotValue))
, effectively creating a “double keccak” pattern typical of mapping(uint256 => mapping(address => uint256))
or similar structures.
balance_of: HashMap[address, uint256]
in the Vyper ContractFrom the posted Yearn V3 Vault Vyper code (omitting unrelated parts):
In Vyper (>=0.2.x), a single-layer mapping is stored as follows:
For instance, if balance_of
happens to be the 7th allocated slot in the contract:
There is no keccak256(abi.encode(18, <address>))
step, and certainly not another nested keccak256(abi.encode(...))
on top of it.
In ScrvusdVerifierV1
, the derived slot for balanceOf(self)
is calculated with keccak256(abi.encode(18, SCRVUSD))
, followed by one more keccak256(abi.encode(...))
.
In the actual Vyper contract, a single-layer balance_of[address]
slot is keccak256(abi.encode(address, baseSlotIndex))
, where baseSlotIndex
depends on the variable’s declaration order (and is unlikely to be 18
).
Because the slot derivations differ fundamentally, the Verifier is reading the wrong location (often defaulting to 0 or some unrelated data).
The code also includes:
If the intended targets are the Vyper Yearn V3 Vault’s total_debt
, total_idle
, and total_supply
, it is highly unlikely that these fields map to slots 21, 22, or 20. The contract’s final layout depends on declaration order, inserted variables, mappings, etc. Consequently, more mismatches are inevitable.
Reading Incorrect Parameters
Downstream logic (e.g., _updatePrice
, _update_profit_max_unlock_time
) that uses these fetched values for valuation, liquidation, or reward settlements will operate on distorted data.
Potential Exploitation by Attackers
Armed with knowledge of these mismatches, an attacker might craft apparently “valid” state proofs that supply unrelated storage slot values, which the contract mistakenly treats as balanceOf(self)
. This can compromise pricing or operational logic.
Broken Verification
The system aims to use state proofs to confirm the Vault’s status at a given block. But because it fails to retrieve the correct storage values, any trust or security mechanism built on these proofs is severely undermined.
Alice and Bob participate in a DeFi protocol that relies on ScrvusdVerifierV1
to read the SCRVUSD
contract’s “balanceOf(self)” in real time—perhaps to update a core oracle or settle certain operations.
A simplified verifier snippet might look like this:
However, SCRVUSD
is actually a Vyper Vault contract where balance_of: HashMap[address, uint256]
is stored at keccak256(abi.encode(address, baseSlotIndex))
, and baseSlotIndex
is not 18
—nor does it need a second nested keccak. Thus, ScrvusdVerifierV1
ends up reading a slot that doesn’t match the real balanceOf(self)
.
Bob, an adversary, recognizes that the Verifier’s calculation for “balanceOf(self)” is off.
Bob creates a forged or specialized state root (assuming he can manipulate “SCRVUSD” storage on a test or forked chain environment) such that the slot at keccak256(abi.encode(18, SCRVUSD))
has an enormous value (e.g., 10^30
).
Bob then generates a “state root + Patricia trie proof (RLP format)” that allows ScrvusdVerifierV1
to read 10^30
from extractSlotValueFromProof
.
verifyScrvusdByBlockHash
Because the contract does not confirm the correct codeHash
or slot index—and may only do superficial checks on blockHash
or chainID
—ScrvusdVerifierV1
incorrectly regards the proof as valid.
Internally, _extractParametersFromProof(...)
calls extractSlotValueFromProof
and retrieves the artificially large value (10^30
) as the “balanceOf(self).”
ScrvusdVerifierV1
then passes the “balanceOf(self) = 10^30” parameter to an external oracle via something like:
If the oracle does not cross-verify these values (e.g., by validating the contract codeHash or the plausibility of the reported balance), it will accept this fabricated data.
Subsequently, any pricing, liquidation, or reward logic is conducted with drastically incorrect figures, which could cause severe damage to normal users (e.g., Alice) by underpricing, overpricing, or miscalculating reserves.
Bob effectively tricks the system into assuming the vault holds far more tokens than reality, distorting the protocol’s liquidation or pricing mechanisms.
This entire exploit hinges on the verifier’s incorrect approach to deriving storage slots for mappings.
Manual Review
Match the Actual Storage Layout
In the Verifier, rely on the true compiled storage layout of the target Vyper contract, ensuring that the correct slot index and key-encoding approach are used.
For example, if balance_of
is actually at slot 7:
Use exactly one keccak invocation keccak256(abi.encode(key, slotIndex))
, without extra nesting or substitutions like uint256(18, SCRVUSD)
.
- 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)
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.