Common Vulnerabilities: Liquid Restaking Protocols - Smart Contracts

Category Security

Common Vulnerabilities: Liquid Restaking Protocols - Smart Contracts

Introduction

Liquid restaking protocols have rapidly gained traction in the DeFi space, transforming how users manage and maximize their staking strategies. By enabling assets to be reused across multiple platforms, these protocols offer increased flexibility and capital efficiency. However, with this innovation comes several exploitable risks.

Developers and security researchers must stay vigilant about these vulnerabilities to strengthen the security of their systems. In this article, we'll explore the prevalent issues that can arise with Liquid restaking protocols. We'll look at recent challenges with EigenLayer integration and other forms of staking protocols to highlight why careful implementation is so important to avoid issues like reentrancy attacks, Denial-of-Service (DoS) vulnerabilities, and incorrect reward distributions.

We'll look at typical security flaws and technical challenges, backed by examples from Sigma Prime's reviews. Our goal is to offer a concise yet thorough overview of common vulnerabilities in Liquid (Re)staking protocols, providing a valuable resource for developers and security researchers in the DeFi space.

What are Liquid Staking/Restaking/EigenLayer Protocols?

Liquid Staking, Restaking, and EigenLayer protocols are advanced mechanisms used within Proof of Stake (PoS) blockchain networks to enhance the liquidity, security, and usability of staked assets. All three protocols are similar in their goal, but they differ in some key aspects.

Liquid Staking

Liquid staking allows users to stake their tokens while retaining liquidity. Instead of locking tokens in a staking contract, users receive derivative tokens which represent their staked assets. These can then be traded, used as collateral, or deployed in other DeFi protocols. This has the added benefit of flexibility of your staked value.

Restaking

Restaking refers to the practice of staking assets across multiple protocols or services. This process involves leveraging the same set of staked tokens to secure additional layers or services within the blockchain ecosystem. Restaking enhances the economic utility of the staked tokens without requiring additional capital from the staker.

EigenLayer Protocols

EigenLayer protocols aim to boost the security of various blockchain services like Layer 2 solutions, bridges, and oracles, which are collectively called Actively Validated Services (AVS). With EigenLayer, users can deposit native ETH or Liquid Staking Tokens (LSTs) and then delegate their stake to trusted operators. These operators, approved by the AVS, use the delegated stake to validate different services. The rewards earned are shared with the users who delegated their stake, but if the operators act dishonestly, the staked amount can be slashed.

Understanding how these protocols work and how they interact with each other is key to spotting and fixing potential issues. In the next sections, we'll look at common vulnerabilities in these protocols, using real-world examples from Sigma Prime's recent reviews to show their impact. Throughout the remainder of this article, we'll refer to all these primitives collectively as Liquid Staking protocols.

EigenLayer Restaking protocols are fairly new and involve several important components and workflows to make the system work. Here’s a breakdown to help understand how they function and where potential issues might arise.

Key Entities

  • Stakers: They can either delegate their tokens to trusted Validators or stake them natively through EigenPods.
  • Validators: Receive delegated stakes from Stakers and participate in validating services. They earn rewards or face penalties based on their performance.
  • Node Operators: Manage their nodes, handle validator operations, and ensure proper interaction with the underlying blockchain network.
  • Delegation Manager: Keeps track of delegated shares, processes undelegations, and manages the withdrawal queue for Stakers.
  • Withdrawal Router: Manages the delayed withdrawal process, making sure assets are properly exited and transferred from the system.

Deposit Paths

  1. Liquid Staking: Stakers deposit LSTs into a strategy managed by the StrategyManager, which are then added to the current delegatee in the DelegationManager.
  2. Native Staking: Involves running validators. Stakers deploy an EigenPod and activate validators on the Beacon Chain. Validator activation is verified with verifyWithdrawalCredentials(), and shares are assigned to the current delegatee in the DelegationManager.

Delegation Process

Shares are not delegated by default and can be delegated in two ways:

  1. Register as an Operator: Operators delegate their shares to themselves permanently.
  2. Delegate to an Operator: Stakers delegate shares to trusted operators. To change delegatee, the staker must first undelegate. Forced undelegations can be initiated by the staker, operator, or a delegation approver set by the operator.

Withdrawal Process

Withdrawals can be initiated from EigenLayer or the Beacon Chain:

  1. Beacon Chain Withdrawals:
    • Full Withdrawal: Validator requests a full withdrawal, and the podOwner validates it on EigenLayer. The withdrawal amount consists of the entire balance.
    • Partial Withdrawal: Validator requests a partial withdrawal, and the funds exit through the DelayedWithdrawalRouter. The withdrawal amount consists of the requested surplus balance above 32 ETH.
  2. EigenLayer Withdrawals:
    • Undelegate and Withdraw: Stakers undelegate and request withdrawals as tokens.
    • Queue Withdrawals: Stakers specify from which strategies to withdraw, and the shares enter a withdrawal queue.

Slashing Mechanism

The slashing mechanism for EigenLayer is not yet enabled. Integrators should prepare for this by ensuring their codebase can handle future slashing events. However, the Beacon Chain does have slashing mechanisms in place which were enabled at launch.

Common Vulnerabilities

We will briefly look into some common issues found in Liquid Staking protocols. These examples are drawn from our reviews to highlight what we typically look for and ask ourselves.

Reentrancy

  • Always update the contract state before making any external calls. Use nonReentrant modifiers to block multiple calls to the same function and follow the Checks-Effects-Interactions (CEI) pattern to ensure state changes happen before interactions.
  • Keep in mind that reentrancy guards also have their limitations, especially when you are dealing with multiple contracts interacting with each other. The guard would work fine in normal circumstances, but it wouldn't stop someone with execution control reentering through a different contract. We should also be careful of contracts that share states as this can also circumvent reentrancy guards.

Loops

  • Unbounded loops can lead to DoS attacks or excessive gas usage. Beware of adding too many tokens, delegations, or withdrawals in a single transaction. We will provide a real-world example of this in the next sections. It shows an elegant example of how mistakes can happen when using multiple loops and how disruptive they can be.
  • Make sure that msg.value is not replayed in loops as it can cause multiple undesired payments to be made.

Deterministic Withdrawal Vault Address Calculations

  • One of our Security Engineers gave a talk on Deterministic Withdrawal Vault Address Calculations at Secureum 2023. In this talk, he went over create2(), some of its intricacies, and what contract metadata entails.
  • Metadata is essentially information about the deployed contract. For example, it includes information such as the EVM version, the compiler settings, the compilation source files/source units, language used, and more. The reason for this data to exist is to enable rigorous verification of the contract's source code. The metadata is stored in the contract's bytecode as a tag at the end of said bytecode. This tag is called the metadata hash.
  • The smallest changes to the contract's metadata cause the bytecode to change. Changing the bytecode will therefore lead to a different contract address created by create2(). This could lead to users being unable to withdraw their funds if the contract address is somewhat different.

Beacon Chain State Root Manipulation

  • Since smart contracts cannot directly access Beacon Chain data (except for Beacon Block roots through EIP-4788), they must rely on oracles or other mechanisms to provide this information. This dependency then adds another layer of possible vulnerabilities as these oracles themselves can be compromised or provide incorrect data either accidentally or maliciously.
  • Based on an incorrect state root, it would allow invalid deposit and withdrawal proofs to be processed and accepted, which would enable unauthorized withdrawals or deposits being recognized as legitimate.

Economic Attacks on Token Value

  • Attackers could use methods like flash loans to skew pool balances, devaluing or overvaluing tokens.
  • We will show a real-world example of this vulnerability in the next sections. This example illustrates how a flaw in the tracking of shares after forced delegations can cause the manipulation of the TVL, which in turn can be exploited to attack the token value.

Preventing Slashing

  • Malicious users might attempt to prevent slashing or liquidations through transaction griefing, out-of-gas errors, or timely withdrawals.
  • One such example can be found in our Mantle Network review. In it, a node can avoid slashing by manipulating their deposit reserves. In Mantle, nodes have to provide a deposit as insurance for their performance. The deposit size is only checked at deposit time. Thus, any reduction to this amount will not impact the node's validity. Therefore, the node can reduce their deposit to zero and avoid slashing.

Merkle Proofs Spoofing

  • One of our Security Assessment Managers gave a talk on Merkle trees and their common pitfalls at Secureum 2023. This is a great resource for understanding the basics of Merkle trees and how they can be exploited.
  • Merkle trees are fundamental to many staking protocols, and their predictable values can be targets for manipulation. If any part of the Merkle tree (e.g., height, leaf, or root) can be altered, malicious actors might be able to spoof Merkle proofs.

Handling Decimals in Calculations

  • Improper management of decimals in financial calculations can cause severe rounding errors that can significantly impact staking outcomes.
  • Programs generally use a fixed number of bits to represent numbers. This means that there is a limit to the number of digits that can be represented. Rounding is often required when division operations are performed to represent the possibly infinite result in a fixed number of bits.
  • An example of this going wrong can be found in the following code snippet, which represents the formula used during the calculation of a mint amount. We observe that inflationPercentage is calculated by performing two separate cases of a multiply and a divide. This oversight causes double rounding losses (once for each divide operation), which reduces the mint amount.
// Calculate the percentage of value after the deposit
uint256 inflationPercentaage = (SCALE_FACTOR * _newValueAdded) / (_currentValueInProtocol + _newValueAdded);

// Calculate the new supply
uint256 newEzETHSupply = (_existingEzETHSupply * SCALE_FACTOR) / (SCALE_FACTOR - inflationPercentaage);

// Subtract the old supply from the new supply to get the amount to mint
uint256 mintAmount = newEzETHSupply - _existingEzETHSupply;

Access Control and State Integrity

  • Using role-based access, limiting the powers of privileged roles, implementing multi-signature controls, and adopting decentralized governance practices lowers the probability of abuses. Even if a modifier is in place, it is important to double-check the logic to ensure it is implemented properly.
  • A good resource on Access Restriction can be found here.

Cooldown Period Exploitations

  • Cooldown periods are designed to manage withdrawals and prevent abuses. However, vulnerabilities in these periods can allow stakers to evade penalties, undermining the system's integrity.

Security of Staking Finalization Processes

  • The finalization process in staking mechanisms must be robust and transparent. Vulnerabilities here can lead to manipulation, preventing proper reward distribution or stake finalization.

Enforcement of Staking Timeframes

  • Validate timestamps on all staking actions and ensure they fall within the appropriate periods to uphold the protocol's specification.

Examples of Vulnerabilities in Sigma Prime's Reviews

To provide a more practical understanding of the vulnerabilities mentioned above, we present some examples from our own reviews of Liquid Staking protocols. These examples highlight real-world bugs and their impact on the security and functionality of these systems.

Incorrect Accounting for stakedButUnverifiedNativeETH

In one of our recent security reviews of Kelp's LRT Smart Contract Updates (KLP2-01), we identified a problem in how the protocol accounts for staked ETH that has not been verified. Accounting issues like this one are far too common, but especially crucial in such a high-stakes environment as staking.

When 32 ETH is staked, a variable called stakedButUnverifiedNativeETH is increased by 32 ETH. After verifying the stake, this amount should be decreased to reflect that the ETH is now verified and accounted for in EigenLayer. However, in this case, the issue arrises during the verification process. Instead of substracting the 32 ETH total, the protocol substracts the "effective balance" of the validator.

Validators can have an effective balance lower than 32 ETH. If this happens, the variable stakedButUnverifiedNativeETH will end up with some leftover ETH that is not actually owned by the protocol. This discrepency would mean that the protocol's total funds are overstated and that it token price could be calculated incorrectly.

In the following code snippet, we can see that the 32 ETH is incremented after calling stake(). However, we can see in the second code snippet that it substracts the effective balance and not the 32 ETH that was staked.

IEigenPodManager eigenPodManager = IEigenPodManager(lrtConfig.getContract(LRTConstants.EIGEN_POD_MANAGER));
eigenPodManager.stake{ value: 32 ether }(pubkey, signature, depositDataRoot);

// tracks staked but unverified native ETH
stakedButUnverifiedNativeETH += 32 ether;
eigenPod.verifyWithdrawalCredentials(
    oracleTimestamp, stateRootProof, validatorIndices, withdrawalCredentialProofs, validatorFields
);

uint256 totalVerifiedEthGwei = 0;
for (uint256 i = 0; i < validatorFields.length;) {
    uint64 validatorCurrentBalanceGwei = BeaconChainProofs.getEffectiveBalanceGwei(validatorFields[i]);
    totalVerifiedEthGwei += validatorCurrentBalanceGwei;
    unchecked {
        ++i;
    }
}

// reduce the eth amount that is verified
stakedButUnverifiedNativeETH -= (totalVerifiedEthGwei * LRTConstants.ONE_E_9);

Infinite Loop Vulnerability in Self-Delegation Function

We identified an issue in the Omni token and AVS review (OMNI-01) involving its _getSelfDelegations() function, which was susceptible to a Denial-of-Service (DoS) condition. This vulnerability arises when operators had staked in a strategy not included in the _strategyParams array. Consequently, the syncWithOmni() function failed if any operator had staked in an incompatible strategy.

The bug is illustrated in the following code snippet:

for (uint256 i = 0; i < strategies.length;) {
    IStrategy strat = strategies[i];

    // find the strategy params for the strategy
    StrategyParam memory params;
    for (uint256 j = 0; j < _strategyParams.length;) {
        if (address(_strategyParams[j].strategy) == address(strat)) {
            params = _strategyParams[j];
            break;
        }
        unchecked {
            j++;
        }
    }

    // if strategy is not found, do not consider it in stake
    if (address(params.strategy) == address(0)) continue;

    staked += _weight(shares[i], params.multiplier);
    unchecked {
        i++;
    }
}

If the current strategy is not found within _strategyParams, the loop continues without incrementing the i iterator. Therefore, the same incompatible strategy is checked repeatedly, causing an infinite loop that only ends when the transaction runs out of gas, ultimately leading to a Denial-of-Service.

Verifying Beacon Chain Withdrawals Break After Deneb

In our review of EigenLayer (EGN3-01), we identified a flaw within the BeaconChainProof.verifyWithdrawal() function. We found that it did not support withdrawals that occured after the Ethereum Deneb upgrade. This resulted in withdrawals being stuck or maliciously fabricated inside EigenLayer's EigenPod contract.

The BeaconChainProofs library assumes a constant tree height of 4. However, the Deneb upgrade added two new fields to the ExecutionPayload container to accomodate for blobs, as part of EIP-4844. This increased the number of fields from 15 to 17 and thus also increasing the tree height from 4 to 5.

What this resulted in was two-fold:

  1. Valid withdrawal proofs that were generated after Deneb were incorrectly verified to be invalid. Normal users would have been unable to withdraw their funds through the EigenPod contract.
  2. It opened up the possibility for a second pre-image attack where a malicious user can pose as an intermediate leaf node to fabricate withdrawal proofs.

Malicious Validator Front-running Attack

This example will cover the findings from this article RocketPool and Lido Frontrunning Bugfix Review.

By design of the Beacon Chain, the deposit contract only requires 1 ETH as a minimum for the deposit to be instantiated, and this first deposit is the one that sets the withdrawal credentials. Its purpose is to allow depositors to make partial payments rather than the lump sum of 32 ETH. However, this opens up a front-running attack vector for pooled staking.

To exploit this issue, a node operator generates two deposit data instances using the same public key but different withdrawal credentials. One instance is the genuine 32 ETH deposit which will be processed by the staking pool and sent to the deposit contract. The other instance is a malicious implementation where only 1 ETH is deposited, and the withdrawal credentials are swapped out with an address in the attackers control.

The attacker then waits for staking pool to ready the 32 ETH to be submitted to the deposit contract using genuine deposit data. However, before the genuine 32 ETH is deposited the attacker front-runs the transaction with their own deposit data directly to the deposit contract.

Here is what the Beacon Chain logic looks like:

def process_deposit(state: BeaconState, deposit: Deposit) -> None:
  # Verify the Merkle branch
  assert is_valid_merkle_branch(
    leaf=hash_tree_root(deposit.data),
    branch=deposit.proof,
    depth=DEPOSIT_CONTRACT_TREE_DEPTH + 1, # Add 1 for the List length mix-in
    index=state.eth1_deposit_index,
    root=state.eth1_data.deposit_root,
)
  # Deposits must be processed in order
  state.eth1_deposit_index += 1
  pubkey = deposit.data.pubkey
  amount = deposit.data.amount
  validator_pubkeys = [v.pubkey for v in state.validators]
  if pubkey not in validator_pubkeys:
    # Verify the deposit signature (proof of possession) which is not checked by the deposit contract_
    deposit_message = DepositMessage(
      pubkey=deposit.data.pubkey,
      withdrawal_credentials=deposit.data.withdrawal_credentials,
      amount=deposit.data.amount,
)
    domain = compute_domain(DOMAIN_DEPOSIT) # Fork-agnostic domain since deposits are valid across forks
    signing_root = compute_signing_root(deposit_message, domain)
    if not bls.Verify(pubkey, signing_root, deposit.data.signature):
      return
   # Add validator and balance entries
   state.validators.append(get_validator_from_deposit(state, deposit))
   state.balances.append(amount)
else:
   # Increase balance by deposit amount
   index = ValidatorIndex(validator_pubkeys.index(pubkey))
   increase_balance(state, index, amount)

Now, since the Beacon Chain registers the withdrawal credentials only on the first valid submission, it is the attacker specified withdrawal credentials which are set for this public key. The second deposit of 32 ETH from the deposit pool then triggers the final else statement (in the code snippet above) to simply increase the validators balance by 32 ETH, ignoring the genuine withdrawal credentials.

The attacker must then only wait for their validator to be activated, then exited, and they will receive 33 ETH to their withdrawal address.

Conclusion

This article has covered some ins and outs of Liquid Staking protocols including Liquid Staking, Restaking and EigenLayer integrations. We went over the components and workflows, common vulnerabilities like exchange rate manipulation, incorrect access control, denial-of-service attacks and more. Real world examples from Sigma Prime’s reviews showed a few of these vulnerabilities in action.

Understanding and mitigating these vulnerabilities is important for developers and security reviewers. It increases the protocols' security and protects users' assets and keeps the overall ecosystem safe and trusted. We encourage all developers and security reviewers to stay informed and be proactive in finding and mitigating these types of threats.

At Sigma Prime, we are committed to securing and hardening Blockchain networks and protocols. If you are building Liquid Restaking solutions and want to harness our cutting-edge security expertise in this area, get in touch.

Further Reading

Here is a list of some of the staking reviews performed by Sigma Prime. This list consists of the reports that have been made public and can also be found in our public reports repository.

Here are some additional write-ups from other security researchers and developers: