Let’s walk through why the most obvious logic bug is in the function _extractParametersFromProof(...)
, specifically in how it computes the “storage slot” key that is passed to Verifier.extractSlotValueFromProof(...)
.
A normal uint256
variable (e.g. uint256 total_debt;
) that the compiler placed at slot N
in storage has its value located directly at key = N
in the Merkle-Patricia trie path. In other words, if you want to prove the value at slot 21
, you pass 21
(RLP-encoded) into the state proof as the path. There is no hashing of 21
—the slot number is simply used as is.
A mapping like
sitting at storage slot M
is a different story. Each entry in the mapping is stored at
So for balanceOf(someAddress)
, the path is the 32-byte value
That is the only time you actually do a keccak256(...)
of the slot plus the “key” (the address, or whatever the mapping key type is).
_extractParametersFromProof(...)
Look at the relevant code snippet:
Notice how the code is doing:
for all slots (21, 22, 20, 38, 39, 40, plus the mapping slot).
But for a normal single-slot variable at, say, slot 21
, the correct path in the MPT is simply 21
(RLP-encoded). We do not do keccak256(21)
.
Only for the mapping slot (e.g., balanceOf(address)
) do we do keccak256(abi.encodePacked(key, slotNumber))
.
By lumping everything together and unconditionally hashing PARAM_SLOTS[i]
, the code is incorrectly computing the MPT path for the normal single-slot variables. It will never find the correct proof for “slot 21” (or 22, 20, etc.) if it’s feeding in the hash of 21
to the Merkle trie path.
Hence, the function _extractParametersFromProof
is where the logic is broken: it forces every slot index through a keccak256(...)
, even for variables that are not from a mapping.
Because the MPT path is wrong for the single-slot variables, those proofs will fail or retrieve zero/garbage. It might look like “the proof is broken” or “the chain data is invalid,” but in reality, it is just that the code is using the wrong key in the Merkle-Patricia trie.
You need to separate out which slots are normal single-slot variables and which ones are mapping entries. For example:
For total_debt
at slot 21, you pass the raw integer 21
(RLP-encoded as a bytes path) into extractSlotValueFromProof(...)
.
For balanceOf(self)
, you do:
and pass that as the path.
That typically means you do something like:
And then call:
The short answer: The function _extractParametersFromProof
has the logical bug. It unconditionally does
for all slots, which is wrong for normal single-slot variables.
If you only hashed the mapping entry slot (balanceOf(...)
) but did not hash the plain integer slots (21
, 22
, etc.), then your proofs for each slot would match the actual storage layout.
See primary comments in issue #23
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.