This repository has been archived by the owner on Sep 8, 2024. It is now read-only.
0xkaden - swapValidatorDetails incorrectly writes keys to memory, resulting in permanently locked beacon chain deposits #84
Labels
Has Duplicates
A valid issue with 1+ other issues describing the same vulnerability
High
A valid High severity issue
Reward
A payout will be made for this issue
Sponsor Confirmed
The sponsor acknowledged this issue is valid
Will Fix
The sponsor confirmed this issue will be fixed
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.
The text was updated successfully, but these errors were encountered: