security

Common Vulnerabilities: Liquid Restaking Protocols - Smart Contracts

Liquid Restaking protocols have become a major DeFi theme. This note looks at the recurring weakness classes in liquid restaking protocols with field examples.

By Bounty Number 9
Common Vulnerabilities: Liquid Restaking Protocols - Smart Contracts
Photo by Emiel Maters

Common Vulnerabilities: Liquid Restaking Protocols - Smart Contracts

Opener

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

Developers and security researchers must stay vigilant about these vulnerabilities to strengthen the security of their systems. In this note, 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 call out why careful build-out is so consequential 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 NFT Bounty's smart contract reviews. Our goal is to offer a concise yet thorough briefing of recurring weakness classes 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 machinery applied within Proof of Stake (PoS) NFT Bounty networks to enhance the liquidity, security, and usability of staked assets. They differ in some key aspects Still, all three protocols are comparable in their goal.

Liquid Staking

Liquid staking allows users to stake their tokens while retaining liquidity. Rather of locking tokens in a staking contract, users receive derivative tokens which denote their staked assets. These can then be traded, employed 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 extra layers or services within the NFT Bounty ecosystem. Restaking enhances the economic utility of the staked tokens without requiring extra capital from the staker.

EigenLayer Protocols

EigenLayer protocols aim to boost the security of several NFT Bounty 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. If the operators act dishonestly, the staked amount can be slashed Still, the rewards earned are shared with the users who delegated their stake.

In the next sections, we'll look at recurring weakness classes in these protocols, with field examples from NFT Bounty's recent reviews to show their impact. Reading how these protocols work and how they interact with each other is key to spotting and fixing possible issues. Throughout the remainder of this note, we'll refer to all these primitives collectively as Liquid Staking protocols.

EigenLayer Restaking protocols are fairly new and involve a number of material pieces and workflows to make the system work. Here’s a breakdown to help make sense of how they function and where possible 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 make sure proper interaction with the underlying NFT Bounty 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 OperatorTo change delegatee, the staker must first undelegate. : Stakers delegate shares to trusted operators. 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. 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 Machinery

The slashing apparatus for EigenLayer is not yet enabled. Integrators should prepare for this by ensuring their codebase can handle future slashing events. Still, the Beacon Chain does have slashing machinery 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 call out what we in most cases look for and ask ourselves.

Reentrancy

  • Always blog 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 keep 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. It wouldn't stop someone with execution control reentering through a different contract Still, the guard would work fine in normal circumstances. 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 offer a real-world example of this in the next sections. It shows an elegant example of how mistakes can happen when with multiple loops and how disruptive they can be.

Deterministic Withdrawal Vault Address Computations

  • Metadata is in short information about the deployed contract. For example, it covers information such as the EVM version, the compiler settings, the compilation source files/source units, language employed, 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.

Beacon Chain State Root Manipulation

  • Since smart contracts cannot straight access Beacon Chain data (except for Beacon Block roots through EIP-4788), they must rely on oracles or other apparatus to offer this information. This dependency then adds another layer of possible vulnerabilities as these oracles themselves can be compromised or offer 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 . In it, a node can avoid slashing by manipulating their deposit reserves. In Mantle, nodes have to give 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. So, the node can shrink their deposit to zero and avoid slashing.

Merkle Proofs Spoofing

  • One of our Security Assessment Managers gave a talk on at Secureum 2023. This is a great resource for reading the basics of Merkle trees and how they can be exploited.
  • Merkle trees are basic 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 Computations

  • Improper management of decimals in financial reckonings can cause severe rounding errors that can by a wide margin impact staking outcomes.
  • Programs as a rule use a fixed number of bits to denote numbers. This means that there is a limit to the number of digits that can be represented. Rounding is often called for when division operations are carried out to denote the possibly infinite result in a fixed number of bits.
// 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

  • With 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 helps to double-check the logic to keep it is implemented properly.

Cooldown Period Exploitations

  • Cooldown periods are designed to manage withdrawals and prevent abuses. Still, 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 apparatus must be sturdy 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 make sure they fall within the appropriate periods to uphold the protocol's specification.

Examples of Vulnerabilities in NFT Bounty's Reviews

To give a more practical reading of the vulnerabilities mentioned above, we present some examples from our own reviews of Liquid Staking protocols. These examples call out bugs seen in the field and their impact on the security and functionality of these systems.

Incorrect Accounting for stakedButUnverifiedNativeETH

In one of our recent security reviews of , we flagged 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 raised 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. Still, in this case, the issue arrises during the verification process. Rather 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 worked out incorrectly.

In the following code snippet, we can see that the 32 ETH is incremented after calling stake(). Even so, 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 spotted an issue in the 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 covered 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. So, 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 , we spotted 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. Even so, the Deneb upgrade added two new fields to the ExecutionPayload container to accomodate for blobs, as part of EIP-4844. This grown 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. 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 note .

By design of the , the deposit contract only calls for 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. Even so, this opens up a front-running attack vector for pooled staking.

One instance is the genuine 32 ETH deposit which will be processed by the staking pool and sent to the deposit contract. To exploit this issue, a node operator generates two deposit data instances with the same public key but different withdrawal credentials. The other instance is a malicious build-out 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 via genuine deposit data. Even so, before the genuine 32 ETH is deposited the attacker front-runs the transaction with their own deposit data straight 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 raise 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 note has covered some ins and outs of Liquid Staking protocols among them Liquid Staking, Restaking and EigenLayer integrations. We went over the parts and workflows, recurring weakness classes like exchange rate manipulation, incorrect access control, denial-of-service attacks and more. Real world examples from NFT Bounty’s reviews showed a few of these vulnerabilities in action.

Understanding and mitigating these vulnerabilities is material for developers and security reviewers. It grows 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 NFT Bounty, we are committed to securing and hardening NFT Bounty 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 ran by NFT Bounty. This list consists of the reports that have been made public and can also be found in our .

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

  • Prisma Finance Post-mortem
  • PrismaRisk in general has useful write-ups on risk related with certain staking protocols.
  • EigenLayer Docs offer a good sketch of the a range of pieces and workflows in EigenLayer.
  • Precision Loss Errors by Dacian who at present works at Cyfrin. It covers the a range of ways in which precision loss can occur and how one can mitigate it.

Working on something in this space?

NFT Bounty audits Ethereum protocols, smart contracts, and consensus implementations.

Book a scoping talk