In ScrvusdVerifierV1.sol
, there's a critical error in how the storage slot for balanceOf(self)
is calculated: #L32-L40
The correct way to calculate a mapping's storage slot in Solidity is
Where
key
is the mapping key (in this case, the SCRVUSD address itself)
mappingSlot
is the slot number of the mapping itself (18)
However, the contract reverses this order.
Tthe verifier contracts ScrvusdVerifierV1 and V2, they both use storage proofs to verify parameters from Ethereum. V1 seems to handle multiple parameters related to the vault's state, like total_debt
, total_idle
. V2 adds a new function to verify the profit_max_unlock_time
parameter. The verifiers interact with a BlockHashOracle
to get Ethereum block hashes or state roots, ensuring the data's authenticity.
The ScrvusdOracleV2 contract in Vyper is the oracle that calculates the price using these verified parameters. It has different versions (v0, v1, v2) which approximate the price with increasing sophistication. The update_price
function is crucial here, as it updates the oracle's state with new parameters from the verifiers. There's also a smoothing mechanism to prevent abrupt price changes, which is important for preventing MEV and manipulation.
The verifiers rely on storage proofs, which are secure if implemented correctly. However, the proofs are constructed and verified. For example, in the _extractParametersFromProof
function in V1, they loop through proofs starting from index 1. The PARAM_SLOTS
array. The first element is 0 (account proof filler), then slots 21, 22, 20, 38, 39, 40, and the keccak of (18, SCRVUSD). Because balanceOf
is a mapping, so the key is the address, and the slot is determined by keccak(abi.encode(address, slot))
. But in the code, PARAM_SLOTS[7]
is keccak(abi.encode(18, SCRVUSD))
.
In Solidity, for a mapping like balanceOf
, the storage slot is computed as keccak(KEY, slot_number)
. Here, the slot for the mapping is 18, and the key is SCRVUSD's address. So the actual slot would be keccak(abi.encodePacked(SCRVUSD, 18))
, but the code uses keccak(abi.encode(18, SCRVUSD))
. The order here matters. Solidity's mapping storage layout computes the slot as keccak(abi.encode(key, mappingSlot)), so if the mapping is at slot 18, then each key's storage is keccak(abi.encode(key, 18)).
But in the code, they use keccak(abi.encode(18, SCRVUSD)), which is keccak(18, address), which would be different. So that's a problem because the order of the encode is (18, SCRVUSD) instead of (SCRVUSD, 18). That would result in an incorrect storage slot for balanceOf(self)
. That's an error because the balanceOf
would be read from the wrong slot, leading to incorrect parameters being passed to the oracle.
The verifier will read from the wrong storage slot, retrieving arbitrary data instead of the actual balanceOf(self)
value
The ScrvusdOracleV2
will receive invalid parameters, causing it to calculate incorrect prices for scrvUSD
Vulnerable Liquidity Pools: Any stableswap-ng pools relying on this oracle will have incorrect price information, potentially allowing attackers to:
Extract value through arbitrage
Drain liquidity from one side of the pool
Manipulate the market to their advantage
This vulnerability affects all chains where the oracle system is deployed (except Ethereum), creating a systemic risk across multiple blockchains
Correct the slot calculation to
- 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.