Open sherlock-admin opened 7 months ago
The protocol team fixed this issue in the following PRs/commits: https://github.com/rio-org/rio-sherlock-audit/pull/2
The protocol team fixed this issue in PR/commit rio-org/rio-sherlock-audit#2.
Fixed Ordering is corrected as per recommendation
The Lead Senior Watson signed off on the fix.
0xkaden
high
swapValidatorDetails incorrectly writes keys to memory, resulting in permanently locked beacon chain deposits
Summary
When loading BLS public keys from storage to memory, the keys are partly overwritten with zero bytes. This ultimately causes allocations of these malformed public keys to permanently lock deposited ETH in the beacon chain deposit contract.
Vulnerability Detail
ValidatorDetails.swapValidatorDetails is used by RioLRTOperatorRegistry.reportOutOfOrderValidatorExits to swap the details in storage of validators which have been exited out of order:
In swapValidatorDetails, for each swap to occur, we load two keys into memory from storage:
The problem here is that when we store the keys in memory, they don't end up as intended. Let's look at how it works to see where it goes wrong.
The keys used here are BLS public keys, with a length of 48 bytes, e.g.:
0x95cfcb859956953f9834f8b14cdaa939e472a2b5d0471addbe490b97ed99c6eb8af94bc3ba4d4bfa93d087d522e4b78d
. As such, previously to entering this for loop, we initialize key1 and key2 in memory as 48 byte arrays:Since they're longer than 32 bytes, they have to be stored in two separate storage slots, thus we do two sloads per key to retrieve
_part1
and_part2
, containing the first 32 bytes and the last 16 bytes respectively.The following lines are used with the intention of storing the key in two separate memory slots, similarly to how they're stored in storage:
The problem however is that the second mstore shifts
_part2
128 bits to the right, causing the leftmost 128 bits to zeroed. Since this mstore is applied only 16 (0x10) bytes after the first mstore, we overwrite bytes 16..31 with zero bytes. We can test this in chisel to prove it:Using this example key:
0x95cfcb859956953f9834f8b14cdaa939e472a2b5d0471addbe490b97ed99c6eb8af94bc3ba4d4bfa93d087d522e4b78d
We assign the first 32 bytes to
_part1
:We assign the last 16 bytes to
_part2
:We assign 48 bytes in memory for
key1
:And we run the following snippet from swapValidatorDetails in chisel:
Now we can check the resulting memory using
!memdump
, which outputs the following:We can see from the memory that at the free memory pointer, the length of key1 is defined 48 bytes (0x30), and following it is the resulting key with 16 bytes zeroed in the middle of the key.
Impact
Whenever we swapValidatorDetails using reportOutOfOrderValidatorExits, both sets of validators will have broken public keys and when allocated to will cause ETH to be permanently locked in the beacon deposit contract.
We can see how this manifests in allocateETHDeposits where we retrieve the public keys for allocations:
We then use the public keys to stakeETH:
Ultimately for each allocation, the public key is passed to the beacon DepositContract.deposit where it deposits to a public key for which we don't have the associated private key and thus can never withdraw.
Code Snippet
Tool used
Manual Review
Recommendation
We can solve this by simply mstoring
_part2
prior to mstoring_part1
, allowing the mstore of_part1
to overwrite the zero bytes from_part2
:Note that the above change must be made for both keys.