Typical vulnerabilities in LSD protocols

Polgemy
Decurity
Published in
14 min readJun 6, 2023

--

Introduction

In this article, we will examine the security aspects of widely used Liquid Staking Derivatives (LSD) protocols. We will explore the fundamental principles of LSD and delve into their architecture and functionality in detail. Furthermore, we will analyze common vulnerabilities that have been identified during audits and real hacks. Finally, we will provide a link to a checklist that can serve as a valuable resource when conducting a security audit of LSD protocols.

What are Liquid Staking Derivatives?

In blockchain networks utilizing the Proof of Stake consensus mechanism, validators lock native tokens as a collateral for producing and validating blocks. The act of locking assets serves as a guarantee of the validator’s integrity.

If a validator creates or validates an “incorrect” block according to the consensus of other participants, they may face penalties, known as slashing.

Validators receive inflationary rewards and transaction fees as compensation for successfully producing and validating blocks. However, the entry barrier to become a validator is often high for regular users. For instance, in the Ethereum network, becoming a validator requires locking 32 ETH and continuously monitoring the node’s stability. Additionally, prior to the Shanghai upgrade that took place on April 12, 2023, validators were unable to withdraw the 32 ETH that was staked.

Liquid staking protocols address these challenges by offering an infrastructure for easy and secure staking without imposing a minimum token requirement. Instead of staking user tokens directly, LSD protocols issue tokens known as derivatives, which possess higher liquidity in the market. These derivative tokens can be utilized in other DeFi protocols, such as lending platforms, as collateral, enabling stakers to enhance their earnings. Importantly, the capital of stakers remains unblocked, providing them with flexibility and accessibility.

Currently, LSD protocols have a Total Value Locked (TVL) of 18.9 billion USD, making them the most prevalent type of protocols in the DeFi ecosystem.

Source: https://dune.com/owen05/lsd-datacheck

The top three market leaders in the LSD space include the Lido, RocketPool, and Frax protocols. Let’s analyze the main differences between these protocols.

Lido Finance

In the first quarter of 2023, Lido had 29 node operators, which raised concerns about centralization risks. However, Lido is currently testing Distributed Validator Technology (DVT) to mitigate these risks by enabling collaboration between both reliable and unreliable node operators. Lido offers two derivatives: stETH, which utilizes a rebase mechanism, and wstETH, a token with an increasing price.

Rocket Pool

Rocket Pool differs significantly from Lido. The protocol enables any user to become a node operator and earn income. Currently, there are over 2,700 node validators in Rocket Pool. To become an operator, one only needs to possess 8 ETH and a fault-tolerant node. In contrast to Lido Finance, Rocket Pool offers a single derivative called rETH with an increasing price.

Frax Ether

Frax Ether (FRAX) operates on a model with two tokens: frxETH and sfrxETH. frxETH serves as an entry point for users within the FRAX ecosystem. Users receive frxETH by depositing ETH, while the deposited ETH is utilized for staking. Rewards from the Curve LP and Convex Farm pools are distributed among frxETH holders, excluding rewards from ETH staking. To earn profits from ETH staking, frxETH holders must stake their frxETH and acquire sfrxETH. Currently, only 38% of frxETH has been exchanged for sfrxETH, as the yield for frxETH stands at 5.1% compared to 4.8% for sfrxETH.

It is noteworthy that despite the Shanghai hard fork enabling the withdrawal of locked ETH, the total amount of locked ETH continues to increase. This indicates the growing popularity of liquid staking.

How it works?

Let’s analyze the algorithm of the LSD protocol using the example of Frax Ether. The protocol involves three primary participants:

  1. Liquidity Providers — users who have deposited ETH for staking.
  2. Node operators — individuals or entities responsible for operating the network nodes.
  3. The DAO that manages the nodes.

Let’s examine the workflow based on the diagram above:

  1. The user initiates the deposit process by sending ETH to the frxETHMinter contract using the submit() method. In return, the user receives frxETH tokens at a 1:1 ratio. It’s important to note that frxETH is a synthetic representation of ETH, similar to WETH. The price of frxETH in relation to ETH remains constant at 1, except in situations where there is insufficient liquidity on decentralized exchanges (DEXs).
  2. FRAX bots periodically execute the depositEther() method on the frxETHMinter contract once the accumulated ETH reaches 32. This method interacts with the getNextValidator() of the OperatorRegistry contract to obtain information about “free” validators. Subsequently, the contract deposits the ETH to the DepositContract address (0x…7705fa), providing the pubKey, withdrawalCredential, signature, and depositDataRoot arguments of the free validator. The DepositContract contract was deployed by the Ethereum Foundation on October 14, 2020, and serves as the primary contract for ETH staking. It is important to note that the addresses of validators are added to the OperatorRegistry contract by the protocol’s governance. Upon detecting the DepositEvent on the DepositContract contract, the consensus layer activates the validator. Once activated, the node operator can launch the node to initiate the validation process.
  3. Users can generate income from staking by exchanging their frxETH for sfrxETH through the sfrxETH contract, which implements the functions of ERC4626.
  4. Validators who participate in block validation receive rewards in ETH, which they send to the FRX Eth Multisig address. The FRX Eth Multisig is a Gnosis Safe contract designed for secure storage and management of funds.
  5. The FRX Eth Multisig contract converts the received ETH into frxETH using the frxETHMinter contract.
  6. The multisig contract then transfers the acquired frxETH to the sfrxETH vault. The exchange rate between frxETH and sfrxETH increases as the number of frxETH tokens in circulation rises while the supply of sfrxETH remains the same.
  7. Bots or users can invoke the syncRewards() method, which facilitates the distribution of frxETH rewards among users based on the ERC4626 standard.
  8. Users have the option to exchange their sfrxETH tokens back to frxETH, allowing them to realize their profits.

After the Shanghai upgrade in Ethereum, validators gained the ability to withdraw their locked ETH from the beacon chain. The detailed process is described in EIP-4895. This EIP introduces a new withdrawal operation at the system level, separate from user operations, which does not require gas and updates the balance without executing operations on the EVM layer. Auditors of LSD protocols must exercise special attention to the handling of withdrawal operations, as this represents a new mechanism that needs to be thoroughly examined.

Unlike the relatively new FRAX protocol, its more established counterparts, Lido and Rocket Pool, have introduced major updates such as v2 and Atlas, respectively. These updates have enabled the option to withdraw the locked collateral in the form of regular ETH instead of derivatives.

The mechanism of providing liquidity in the pool of derivatives

Before the Shanghai update, users could only withdraw their locked ETH by exchanging derivatives for the underlying token. However, to ensure minimal slippage during this process, it is crucial to have a significant amount of liquidity in the derivative token / underlying token pool. To encourage Liquidity Providers to continuously add liquidity to these pools, protocols have developed incentive mechanisms.

Among these protocols, the Lido protocol has implemented one of the most successful LP incentive mechanisms. Currently, the stETH pool in Curve is considered the largest pool in DeFi, with a Total Value Locked (TVL) of 1.16 billion USD.

In the stETH pool LP providers can receive additional rewards in the form of LDO tokens. A similar incentive mechanism is implemented in the 1inch stETH/ETH pool.

Analysis of bugs from LSD audit reports

This section will examine smart contract bugs that have been identified during audits of the LSD protocols. Additionally, a check-list based on these vulnerabilities will be provided below.

Frax Ether Audit (November, 2022)

  • An auditor identified a vulnerability that allows a malicious node operator to access user funds. This vulnerability is associated with the specific processing of the DepositEvent in the consensus mechanism. The issue arises from the initial deposit through the DepositContract, where the WithdrawCredentials and pubkey are fixed and cannot be modified in the future. If an attacking node operator frontruns a transaction from the LSD protocol, specifically the DepositContract.deposit() function (when the accumulated ETH amount reaches 32 and a new validator is activated), and specifies their own WithdrawCredentials instead of those designated by the protocol, the attacker’s WithdrawCredentials will remain in effect during the execution of the main transaction. The only requirement for the attacker is to send a minimum of 1 ETH. Consequently, the attack results in 33 ETH (minus 1 ETH sent by the attacker) being directed to the attacker’s WithdrawCredentials address, leaving 32 LSD protocol users affected. It is worth noting that this vulnerability was discovered as early as 2019.
  • If a validator violates the consensus rules and the violation is agreed upon by the consensus, a slashing penalty is imposed. The minimum penalty for violating the consensus is 1/32 of the balance of the offending validator. In the case of an attack involving multiple offending validators, the penalty depends on the number of such attacks within a period of 4096 epochs from the moment of the first violation. This mechanism is designed to scale the fines accordingly.
    An auditor has discovered a vulnerability where frxETH could become depegged from ETH due to a reduced amount of ETH resulting from slashing penalties. The derivative mechanism should consider the possibility that the number of ETH may be lower than the issued derivatives.
  • An auditor identified a vulnerability related to the risk of centralization. In the case of the FRAX contract, the owner had the ability to add or remove validators from the waiting list, designate the address with the Minter role for frxETH, and perform other actions. Given the substantial Total Value Locked (TVL) in LSD protocols, it is crucial to ensure that a single address does not have the ability to control the entire protocol.
  • It’s common knowledge that an unbounded for loop can result in an out-of-gas error. The auditors discovered two such issues in FRAX.
    The first issue is related to the cyclic call of DepositContract.deposit(). The number of iterations in this call is determined by the amount of accumulated ETH divided by 32. When a large amount of ETH is accumulated, this can result in an out-of-gas error. Here is a vulnerable depositEther function:
// code: https://github.com/code-423n4/2022-09-frax/blob/dc6684f77b4e9bd965e8862be7f5fb71473a4c4c/src/frxETHMinter.sol#L129
// possible out-of-gas error in for() loop if address(this).balance is too large

function depositEther() external nonReentrant {
// ....
// calculating iteractions count
uint256 numDeposits = (address(this).balance - currentWithheldETH) / DEPOSIT_SIZE;
require(numDeposits > 0, "Not enough ETH in contract");

for (uint256 i = 0; i < numDeposits; ++i) {
// ...
// call deposit()
depositContract.deposit{value: DEPOSIT_SIZE}(
pubKey,
withdrawalCredential,
signature,
depositDataRoot
);
activeValidators[pubKey] = true;
emit DepositSent(pubKey, withdrawalCredential);
}
  • The second error occurs when iterating through the original_validators[] array. If there is a large number of validators, it can also lead to an out-of-gas error due to the excessive gas consumption during the iteration process:
// code: https://github.com/code-423n4/2022-09-frax/blob/55ea6b1ef3857a277e2f47d42029bc0f3d6f9173/src/OperatorRegistry.sol#LL91C1-L122C6
// if original_validators[] array is too large out-of-gas is possible
function removeValidator(uint256 remove_idx, bool dont_care_about_ordering) public onlyByOwnGov {
// ...
Validator[] memory original_validators = validators;

// Clear the original validators list
delete validators;

// Fill the new validators array with all except the value to remove
for (uint256 i = 0; i < original_validators.length; ++i) {
if (i != remove_idx) {
validators.push(original_validators[i]);
}
}
// ...
}

GoGoPool Audit (December, 2022)

GoGoPool is the first liquid staking protocol designed for Avalanche subnets. The protocol allows operators to launch nodes at half the cost using the GGP token. Node operators create a minipool in which AVAX stakers are utilized for block production. Stakers receive ggAVAX derivatives when they deposit AVAX. It is important to note that the price of ggAVAX/AVAX is monotonically increasing over time.

  • An auditor discovered a “first deposit” vulnerability related to the ERC4626 standard. An attacker can deposit a minimal amount of 1 wei AVAX to the ggAVAX contract immediately after its creation, thereby setting the totalAssets and totalShares to 1. By subsequently transferring 1000 AVAX (e.g. via selfdestruct) to the ggAVAX contract, the attacker can inflate the totalAssets value significantly while keeping the totalShares value unchanged. This results in an increased price per share. The price recalculation occurs when the syncRewards() function is called. As a consequence, subsequent user deposits will be rounded up to 0 shares, allowing the attacker to maliciously claim user funds. To mitigate this common vulnerability, it is recommended to create the pool simultaneously with the token deposit.
  • Node operators in GoGoPool are required to deposit 10% of 1000 AVAX as GGP tokens, which serve as a guarantee for their legitimate operation as validators. In the event that a validator violates the protocol rules, the GGP tokens can be slashed. A vulnerability was discovered in the Staking contract that allows validators to avoid receiving a fine. The issue arises from the fact that the contract does not check the balance of the GGP tokens before applying the slashing mechanism. Consequently, if the penalty amount exceeds the current balance of the validator, the slashGGP() function will trigger a revert(). This vulnerability enables operators to create a minipool in such a way that the minimum slashing amount will always be greater than the amount of GGP tokens held, effectively bypassing the penalty mechanism. Here is a snippet of vulnerable code:
// code: https://github.com/code-423n4/2022-12-gogopool/blob/aec9928d8bdce8a5a4efe45f54c39d4fc7313731/contracts/contract/Staking.sol
// error related to the lack of checking of the current balance of GGP tokens

/// @notice Minipool Manager will call this if a minipool ended and was not in good standing
/// @param stakerAddr The C-chain address of a GGP staker in the protocol
/// @param ggpAmt The amount of GGP being slashed
function slashGGP(address stakerAddr, uint256 ggpAmt) public onlySpecificRegisteredContract("MinipoolManager", msg.sender) {
Vault vault = Vault(getContractAddress("Vault"));
decreaseGGPStake(stakerAddr, ggpAmt);
vault.transferToken("ProtocolDAO", ggp, ggpAmt);
}
/// @notice Decrease the amount of GGP a given staker is staking
/// @param stakerAddr The C-chain address of a GGP staker in the protocol
function decreaseGGPStake(address stakerAddr, uint256 amount) internal {
int256 stakerIndex = requireValidStaker(stakerAddr);
subUint(keccak256(abi.encodePacked("staker.item", stakerIndex, ".ggpStaked")), amount);
}

LSD Network —Stakehouse Audit (November, 2022)

Stakehouse is an infrastructure protocol for Ethereum staking. StakeHouse allows operators to launch nodes quickly and simply. StakeHouse keep records of deposited ETH on DepositContract, as well as issue derivatives (dETH and sETH tokens) for deposited ETH. dETH token determines the right to receive rewards from ETH staking while sETH determines the right to receive rewards from MEV operations.

Stakers can deposit ETH to an individual validator or a group of validators. With Stakehouse there is also the possibility of a deposit in the “Giant Pool. There are two Giant Pools in the protocol: “MEV & Fees” and “Protected Stakes”. Deposited ETH in Giant Pools are distributed among validators.

The “MEV & Fees” option provides an opportunity to earn income from both transaction fees and MEV operations. However, it’s important to note that this option carries a risk of slashing, which could result in a loss of funds.

On the other hand, the second option, known as “Protected Stakes”, offers stakers the ability to claim only the generated profits from block validation. By choosing this option, stakers eliminate the risk of losing their funds entirely.

  • An auditor discovered a classic possibility of the reentrancy attack in the withdrawETHForKnot() function which allows validators to withdraw their staked ETH. The vulnerability was possible due to a violation of the “checks-effects-interactions” pattern: the mapping of bannedBLSPublicKeys was updated after the transfer of ETH to the validator’s address. This allowed the validator to issue withdrawals repeatedly:
// code: https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/LiquidStakingManager.sol#L326
// mapping bannedBLSPublicKeys edits after ETH sending.
// require(isBLSPublicKeyBanned(_blsPublicKeyOfKnot) == false) check will pass if you log in again

/// @notice Allow node runners to withdraw ETH from their smart wallet. ETH can only be withdrawn until the KNOT has not been staked.
/// @dev A banned node runner cannot withdraw ETH for the KNOT.
/// @param _blsPublicKeyOfKnot BLS public key of the KNOT for which the ETH needs to be withdrawn
function withdrawETHForKnot(address _recipient, bytes calldata _blsPublicKeyOfKnot) external {
require(_recipient != address(0), "Zero address");
require(isBLSPublicKeyBanned(_blsPublicKeyOfKnot) == false, "BLS public key has already withdrawn or not a part of LSD network");

address associatedSmartWallet = smartWalletOfKnot[_blsPublicKeyOfKnot];
require(smartWalletOfNodeRunner[msg.sender] == associatedSmartWallet, "Not the node runner for the smart wallet ");
require(isNodeRunnerBanned(nodeRunnerOfSmartWallet[associatedSmartWallet]) == false, "Node runner is banned from LSD network");
require(associatedSmartWallet.balance >= 4 ether, "Insufficient balance");
require(
getAccountManager().blsPublicKeyToLifecycleStatus(_blsPublicKeyOfKnot) == IDataStructures.LifecycleStatus.INITIALS_REGISTERED,
"Initials not registered"
);

// refund 4 ether from smart wallet to node runner's EOA
IOwnableSmartWallet(associatedSmartWallet).rawExecute(
_recipient,
"",
4 ether
);

// update the mapping
bannedBLSPublicKeys[_blsPublicKeyOfKnot] = associatedSmartWallet;

emit ETHWithdrawnFromSmartWallet(associatedSmartWallet, _blsPublicKeyOfKnot, msg.sender);
}
  • The auditor identified a vulnerability related to the update of the claimed[_user][_token] variable in the _distributeETHRewardsToUserForToken() function which is used to distribute ETH to LP providers in proportion to their shares. Instead of adding a claimed value to the mapping _distributeETHRewardsToUserForToken() assigns a new amount in the claimed[][] mapping every call. As a result the variable stores only the last claimed value. The attacker can repeat the call until the vault is exhausted. Here is a vulnerable _distributeETHRewardsToUserForToken function:
// code: https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/SyndicateRewardsProcessor.sol#LL50C1-L73C6
// The claimed[][] mapping should be populated, not assigned a new value

/// @dev Any due rewards from node running can be distributed to msg.sender if they have an LP balance
function _distributeETHRewardsToUserForToken(
address _user,
address _token,
uint256 _balance,
address _recipient
) internal {
require(_recipient != address(0), "Zero address");
uint256 balance = _balance;
if (balance > 0) {
// Calculate how much ETH rewards the address is owed / due
uint256 due = ((accumulatedETHPerLPShare * balance) / PRECISION) - claimed[_user][_token];
if (due > 0) {
claimed[_user][_token] = due;

totalClaimed += due;

(bool success, ) = _recipient.call{value: due}("");
require(success, "Failed to transfer");

emit ETHDistributed(_user, _recipient, due);
}
}
}

Rocket Pool Audit by Consensys (April, 2021)

  • The rETH token contract allowed burning rETH by sending ETH. At the same time, the number of returned ETH was determined by the getEthValue() function which obtained the price from the oracles. RocketPool did not calculate the ETH/rETH price inside the protocol. Instead, the exchange rate was calculated by the oracles of the rocketDAONodeTrusted contract. During the audit, a critical vulnerability was discovered that was caused by the possibility of a sandwich attack: a price update could be sandwiched between an ETH → rETH deposit transaction and a burn() rETH → ETH transaction.

Rocket Pool Audit by Consensys (January, 2023)

  • A typical error was found in the RocketNodeDistributorDelegate contract in the distribute() function that distributes ETH among node operators and users. The address of the node operator is set in the setWithdrawalAddress() function, which can be called by any user. The distribute() function was not protected by the nonReentrant modifier. Attack vector consisted in setting a malicious smart contract as a withdrawal address and subsequent exploitation of a reentrancy. The error could allow a malicious user drain the whole ETH balance.

The checklist

Last time we approached such DeFi pattern as collateralized debt positions (CDP):

We also have started compiling checklists for the auditors for each examined DeFi pattern:

As a result of this analysis we have compiled the audit checklist for the liquid staking protocols here: https://github.com/Decurity/audit-checklists/blob/master/lsd.md. This list includes only more or less universal issues, however each LSD protocol has nuanced features that cannot be generalized (such as Lido v2).

We hope that auditors will find it useful in LSD audits. We also have scheduled several other DeFi patterns to be examined next, but if you have any suggestions do not hesitate to ping us on Twitter: https://twitter.com/DecurityHQ

--

--