
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.
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.

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 rETHwithan 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.

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

Let’s examine the workflow based on the diagram above:
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).depositEther() method on the frxETHMinter contract once the accumulated ETH reaches 32. This method interacts with the getNextValidator() of the OperatorRegistrycontract 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.syncRewards() method, which facilitates the distribution of frxETH rewards among users based on the ERC4626 standard.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.
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.
Join Medium for free to get updates from this writer.
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.
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.
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.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.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);
}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 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.
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.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);
}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.
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);
}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);
}
}
}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 rocketDAONodeTrustedcontract. 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.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.Last time we approached such DeFi pattern as collateralized debt positions (CDP):
blog.decurity.io
We also have started compiling checklists for the auditors for each examined DeFi pattern:
github.com
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
The researchers who write this are the ones who run the audits.