One Tiny Error, Massive Impact: Inside EigenPods' Critical Merkle Bug

Security

Feb 6, 2025

critical-eigenpods-bug-image

Introduction

In this article, we will illuminate one of the interesting bugs we spotted during the security review for EigenLayer, which was performed in October-November 2023.

The audit scope includes new versions of EigenPod functionalities, introducing novel ideas to the existing protocol.

In total, this audit report has uncovered 14 issues, including 3 criticals. The full report is available to read here, and one of those criticals, EIG-10, is something that we want to tell you about below.

EigenLayer & Eigen Pods

EigenLayer is a protocol that introduces the concept of "restaking." In a traditional proof‐of‐stake (PoS) system, validators lock up ETH to secure the network and earn rewards.

EigenLayer takes this a step further by allowing stakers to restake their already staked ETH (or liquid staking tokens) to provide security for additional networks and decentralized applications.

A key component in enabling this mechanism is the EigenPod—a dedicated smart contract associated with the user's wallet. When a user opts into EigenLayer's restaking process, an EigenPod is created to serve as the management hub for these restaked assets. It redirects the user's staked ETH from a typical validator withdrawal address to the EigenLayer system, allowing the protocol to enforce any extra penalties (or "slashing") incurred by secondary services.

EigenPods not only facilitate staking but also monitor user balances and manage withdrawals. To create a stake on the Ethereum mainnet through EigenLayer, a user first creates an EigenPod and generates a proof of the stake deposit.

This proof is then sent to the EigenPod for verification. Upon successful verification, the user's internal balances are updated accordingly, ensuring the restaking process is secure and transparent.

Merkle trees

Before going into the bug, let's have a quick refresher about Merkle trees.

Merkle trees are special binary trees. Each leaf represents arbitrary data, but the leaf itself only includes a hash of the data.

Each intermediary node in the tree is a hash of the node's two children - the same process repeats all the way up to the root node, which is also a hash of its two children.

In blockchains, all of the hashes of the tree are usually stored somewhere off-chain, and only the root hash is stored on-chain.

Merkle trees can be used to cheaply prove inclusion of data (or, more precisely, a hash of data). Verifying that a given hash is part of the tree only requires a logarithmic amount of checks compared to the tree's height. This is a very useful feature, especially in environments with limited computational capacities - such as blockchains.

Multiple trees

Ethereum utilizes nested Merkle trees in its beacon chain for tracking balances. When a user wants to withdraw their stake, they have to provide proof of their balance for their EigenPod, based on data in the trees.

Multiple Merkle trees are nested by making the leaf of one tree the root of another tree. This way the different trees can be traversed (verified) with one Merkle proof.

Each tree verifies a part of the required data. The user has to provide the required Merkle proof for the correct path through the interleaved trees. If any of the data is incorrect, the verification fails and the user is not allowed to make the withdrawal.

Note that the verification itself is fully public: anyone can execute it for any data. Authorization is handled by other means.

Merkle tree diagram

A bit string

A Merkle proof is a list of hashes, indicating the path taken to reach the node the proof is meant for. To denote the path for the proofs, two things are provided:

  1. A bit string to denote the path in the different trees. This is to pack all the paths in one integer.
  2. Variables to denote how many bits should be utilized for which tree.

As an example, let's say we have two trees. The first has 8 leaves and the second tree has 4 leaves. The second tree's root is the fourth leaf of the first tree.

Say we want to traverse the tree to the second leaf of the second tree. We'd need to give a bit string that denotes the right route. Here's how it can be formed:

  1. We start from the topmost root. From there, we can see that we should go left. So the first bit is 0, denoting left.
  2. After that we go right, so 1.
  3. Another right, so 1. We reach the root of the second tree.
  4. Left, so 0.
  5. And a final right, adding 1.

Bit string path diagram

The full bit string is, therefore, 01101. And we also need to tell the function to take the first three bits for the first tree and two bits for the second tree.

Check the lengths

Since all of this is arbitrary user input, they can give whatever data they want.

So what happens if they provide invalid bit lengths? What if the user tells the function to take two bits for the first tree and three for the second?

Invalid bit lengths diagram

Since the second tree only has 4 leaves, it can't utilize the first bit. It basically just discards it. And suddenly there are not enough bits for the first tree - we only have two bits left for the first tree, even if the tree requires three bits.

However, due to how integers work in Solidity, this is not a problem for the function. It simply thinks the missing bits are zero, so it basically utilizes bits "010". Therefore, both trees are traversed properly, but we end up at a totally different leaf than intended!

This is why the contract includes checks for the bit lengths. There are checks to make sure that the amount of bits we take for each tree is correct, based on the tree's height.

Except that one of these checks is missing.

Providing mismatched bit lengths

Since one of the checks is missing, an attacker can divert the traversal path to an arbitrary path. They only need a valid Merkle proof for the new route.

In our earlier example, we would end up in an erroneous path 01001. However, even if the bug didn't exist, the attacker could just provide this path, valid bit lengths and the correct Merkle proof and end up in the same leaf. So what makes this bug so interesting?

So far, we have omitted one important detail: constant values for the tree sizes (and bit lengths).

The trees have constant bit lengths so the user can't freely provide the lengths. For example, our first tree would have a constant bit length of 3.

Constant bit lengths diagram

The trees should only be traversable in a predefined way. For the tree that is missing the bit length check, users can alter the traversed path, enabling routes that are not meant to be traversed. By providing properly crafted bit lengths, the attacker has a lot of power in defining the traversed path.

In reality, instead of two trees, there are actually five different trees that are connected. But the basic idea remains the same - it's just much easier to understand the concept with a simplified example.

What can an attacker do?

Traversing undesired paths can lead to unpredictable results. But an attacker can only influence the path; they don't have arbitrary control over it. However, they can analyze in advance exactly what can be achieved and what not.

An attacker can validate proofs for assets they haven't deposited. This leads to two, critical, attack vectors:

  1. An attacker can permanently lock assets (ETH) for other stakers
  2. An attacker can gain unwarranted yield from the protocol by pretending to have deposited assets

Conclusions

This bug vividly reminds us that even small oversights can have big consequences in blockchain protocols. While code reviews and testing are essential, security must be treated as an ongoing mindset—not just a box to check off.

Never be stingy with audits. Security is more than just an audit report—it's a commitment to protecting your protocol and its users at every layer and the project lifecycle step.

Mitigate your risk today and get a quote from Hexens because investing in peace of mind is always the best move.