Zokyo Auditing Tutorials
  • 🔐Zokyo Auditing Tutorials
  • 📚Tutorials
    • 🏃Tutorial 1: Front-Running
      • 🚀Prerequisites
      • 📘Understanding Front-Running
      • 👓Examples
      • ⚒️Mitigation Steps
      • 🏦Resource Bank to more front running examples
      • 🤝Front-Running Conclusion
    • 🧱Tutorial 2: Unsafe Casting
      • 🚀Prerequisites
      • 📘Understanding Casting
      • 👓Examples
      • 🤝Unsafe Casting Conclusion
    • 👍Tutorial 3: Approvals and Safe Approvals
      • 🚀Prerequisites
      • 📘Understanding Approvals
      • 👓Vulnerability Examples
        • 🔁ERC20 Approval Reset Requirement
        • 😴Ignoring Return Values from ERC20 approve() Function: Potential Miscount of Successful Approvals
        • 🚫Improper use of Open Zeppelins safeApprove() for Non-zero Allowance Increments
        • 🥾Omitted Approval for Contract Interactions Within a Protocol
        • 🤦‍♂️Failing to Reset Token Approvals in Case of Failed Transactions or other actions
        • 💭Miscellaneous
        • ERC20 Approve Race Condition Vulnerability
      • ⚒️Spot the Vulnerability
      • 🤝Approvals and Safe Approvals Conclusion
    • ⛓️Tutorial 4: Block.chainid, DOMAIN_SEPARATOR and EIP-2612 permit
      • 🚀Prerequisites
      • 📘Understanding Block.chainid and DOMAIN_SEPARATOR
      • 👓Examples
      • ⚒️General Mitigation Steps
      • 🤝Tutorial 4 Conclusion
  • 💰Tutorial 5: Fee-On-Transfer Tokens
    • 🚀Prerequisites
    • 📘Understanding Fee-On-Transfer
    • 👓Examples
    • 📘Links to more fee-on-transfer vulnerability examples
    • 🤝Fee-On-Transfer Tokens: Conclusion
  • 🌴Tutorial 6: Merkle Trees
    • 🚀Prerequisites
    • 📘Understanding Merkle Trees
    • 🔎Verification within a Merkle Tree:
    • 📜Merkle Proofs Within Smart Contracts
    • 🖋️Merkle Proof Solidity Implementation
    • 🛑Vulnerabilities When Using Merkle Trees
    • 💀Example Vulnerabilities
    • 🧠Exercise
    • 🤝Merkle Trees Conclusion
  • 🌳Tutorial 7: Merkle-Patricia Trees
    • 🚀Prerequisites
    • 📘Understanding Merkle-Patricia Trees
    • 📕Understanding Merkle-Patrica Trees pt.2
    • 🔎Verification within a Merkle-Patricia Tree
    • 🛑Vulnerabilities When Using Merkle-Patricia Trees
    • 💀Example Vulnerability
    • 🤝Merkle-Patricia Trees: Conclusion
  • 🔁Tutorial 8: Reentrancy
    • 🚀Prerequisites
    • 📘Understanding Reentrancy
    • ⚒️Mitigation
    • 💀The DAO Hack: An In-depth Examination
    • 👓Examples
    • 🏦Resource Bank To More Reentrancy Examples
    • 🤝Conclusion: Reflecting on the Reentrancy Vulnerability
  • 🔂Tutorial 9: Read-Only Reentrancy
    • 🚀Prerequisites
    • 📘Understanding Read-Only Reentrancy
    • 🔨Mitigating Read-Only Reentrancy
    • 👓Real World Examples
    • 🏦Resource Bank To More Reentrancy Examples
    • 🤝Read-Only Reentrancy: Conclusion
  • 🚆Tutorial 10: ERC20 transfer() and safeTransfer()
    • 🚀Prerequisites
    • 📘Understanding ERC20 transfer() and safeTransfer()
    • 👓Examples
    • 🤝Conclusion
  • 📞Tutorial 11: Low level .call(), .transfer() and .send()
    • 🚀Prerequisites
    • 📘Understanding .call, .transfer, and .send
    • 🛑Understanding the Vulnerabilities of .transfer and .send
    • 👓Examples
    • 🤝Low level .call(), .transfer() and .send() conclusion
  • ☎️Tutorial 12: Delegatecall Vulnerabilities in Precompiled Contracts
    • 🚀Prerequisites
    • 📳Understanding Delegatecall
    • ⛰️EVM, L2s, Bridges, and the Quest for Scalability
    • 🏗️Understanding Precompiles in the Ethereum Virtual Machine (EVM)
    • 💻Custom Precompiles
    • 💀Potential Vulnerabilities in EVM Implementations: Overlooked DelegateCall in Precompiled Contracts
    • 👓Real World Examples
    • 🤝Delegatecall and Precompiles: Conclusion
  • 🌊Tutorial 13: Liquid Staking
    • 🚀Prerequisites
    • 📘Understanding Liquid Staking
    • 💀Understanding Liquid Staking Vulnerabilities
    • 🛑Example Vulnerability
    • 🐜Example Vulnerability 2
    • 🕷️Example Vulnerability 3
    • 🤝Liquid Staking: Conclusion
  • 🚿Tutorial 14: Slippage
    • 🚀Prerequisites
    • 📘Understanding Slippage in Automated Market Makers (AMMs)
    • 💀Understanding the "Lack of Slippage Check" Vulnerability in Automated Market Makers (AMMs) and DEXs
    • 😡On-Chain Slippage Calculations Vulnerability
    • 📛0 slippage tolerance vulnerability
    • 👓Real World Examples
    • 🏦Resource bank to more slippage vulnerabilities
    • 🤝Slippage Conclusion
  • 📉Tutorial 15: Oracles
    • 🚀Prerequisites
    • 📘Understanding Oracles
    • 📈Types of price feeds
    • 😡Flash Loans
    • 💀Understanding Oracle Vulnerabilities
      • ⛓️The Danger of Single Oracle Dependence
      • ⬇️Using Deprecated Functions
      • ❌Lack of return data validation
      • 🕐Inconsistent or Absent Price Data Fetching/Updating Intervals
    • 🔫Decentralized Exchange (DEX) Price Oracles Vulnerabilities
    • 🛑Found Vulnerabilities In Oracle Implementations
      • ⚖️Newly Registered Assets Skew Consultation Results
      • ⚡Flash-Loan Oracle Manipulations
      • ⛓️Relying Only On Chainlink: PriceOracle Does Not Filter Price Feed Outliers
      • ✍️Not Validating Return Data e.g Chainlink: (lastestRoundData)
      • 🗯️Chainlink: Using latestAnswer instead of latestRoundData
      • 😭Reliance On Fetching Oracle Functionality
      • 🎱Wrong Assumption of 18 decimals
      • 🧀Stale Prices
      • 0️⃣Oracle Price Returning 0
      • 🛶TWAP Oracles
      • 😖Wrong Token Order In Return Value
      • 🏗️miscellaneous
    • 🤝Oracles: Conclusion
  • 🧠Tutorial 16: Zero Knowledge (ZK)
    • 🚀Prerequisites
    • 📚Theory
      • 🔌Circom
      • 💻Computation
      • 🛤️Arithmetic Circuits
      • 🚧Rank-1 Constraint System (R1CS)
      • ➗Quadratic Arithmetic Program
      • ✏️Linear Interactive Proof
      • ✨ZK-Snarks
    • 🤓Definitions and Essentials
      • 🔑Key
      • 😎Scalar Field Order
      • 🌳Incremental Merkle Tree
      • ✒️ECDSA signature
      • 📨Non-Interactive Proofs
      • 🏝️Fiat-Shamir transformation (or Fiat-Shamir heuristic)
      • 🪶Pedersen commitment
    • 💀Common Vulnerabilities in ZK Code
      • ⛓️Under-constrained Circuits
      • ❗Nondeterministic Circuits
      • 🌊Arithmetic Over/Under Flows
      • 🍂Mismatching Bit Lengths
      • 🌪️Unused Public Inputs Optimized Out
      • 🥶Frozen Heart: Forging of Zero Knowledge Proofs
      • 🚰Trusted Setup Leak
      • ⛔Assigned but not Constrained
    • 🐛Bugs In The Wild
      • 🌳Dark Forest v0.3: Missing Bit Length Check
      • 🔢BigInt: Missing Bit Length Check
      • 🚓Circom-Pairing: Missing Output Check Constraint
      • 🏹Semaphore: Missing Smart Contract Range Check
      • 🔫Zk-Kit: Missing Smart Contract Range Check
      • 🤖Aztec 2.0: Missing Bit Length Check / Nondeterministic Nullifier
      • ⏸️Aztec Plonk Verifier: 0 Bug
      • 🪂0xPARC StealthDrop: Nondeterministic Nullifier
      • 😨a16z ZkDrops: Missing Nullifier Range Check
      • 🤫MACI 1.0: Under-constrained Circuit
      • ❄️Bulletproofs Paper: Frozen Heart
      • 🏔️PlonK: Frozen Heart
      • 💤Zcash: Trusted Setup Leak
      • 🚨14. MiMC Hash: Assigned but not Constrained
      • 🚔PSE & Scroll zkEVM: Missing Overflow Constraint
      • ➡️PSE & Scroll zkEVM: Missing Constraint
      • 🤨Dusk Network: Missing Blinding Factors
      • 🌃EY Nightfall: Missing Nullifier Range Check
      • 🎆Summa: Unconstrained Constants Assignemnt
      • 📌Polygon zkEVM: Missing Remainder Constraint
    • 💿ZK Security Resources
  • 🤝Tutorial 17 DEX's (Decentralized Exchanges)
    • 🚀Prerequisites
    • 📚Understanding Decentralized Exchanges
    • 💀Common Vulnerabilities in DEX Code
      • 🔎The "Lack of Slippage Check" Vulnerability in Automated Market Makers (AMMs) a
      • 😡On-Chain Slippage Calculations Vulnerability
      • 📛Slippage tolerance vulnerability
      • 😵How Pool Implementation Mismatches Pose a Security Risk to Decentralized Exchanges (DEXs)
      • 🏊‍♂️Vulnerabilities in Initial Pool Creation - Liquidity Manipulation Attacks
      • 🛑Vulnerabilities In Oracle Implementations
        • ⚖️Newly Registered Assets Skew Consultation Results
        • ⚡Flash-Loan Oracle Manipulations
        • ⛓️Relying Only On Chainlink: PriceOracle Does Not Filter Price Feed Outliers
        • ✍️Not Validating Return Data e.g Chainlink: (lastestRoundData)
        • 🗯️Chainlink: Using latestAnswer instead of latestRoundData
        • 😭Reliance On Fetching Oracle Functionality
        • 🎱Wrong Assumption of 18 decimals
        • 🧀Stale Prices
        • 0️⃣Oracle Price Returning 0
        • 🛶TWAP Oracles
        • 😖Wrong Token Order In Return Value
        • 🏗️miscellaneous
      • 🥶Minting and Burning Liquidity Pool Tokens
      • 🎫Missing Checks
      • 🔞18 Decimal Assumption
        • 📌Understanding ERC20 Decimals
        • 💀Examples Of Vulnerabilities To Do With Assuming 18 Decimals
      • 🛣️Incorrect Swap Path
      • The Importance of Deadline Checks in Swaps
    • 🤝Conclusion
  • 🤖Tutorial 18: Proxies
    • 🚀Prerequisites
    • 📥Ethereum Storage and Memory
    • 📲Ethereum Calls and Delegate Calls
    • 💪Upgradability Patterns in Ethereum: Enhancing Smart Contracts Over Time
    • 🔝Proxy Upgrade Pattern in Ethereum
    • 🌀Exploring the Landscape of Ethereum Proxies
      • 🪞Transparent Proxies
      • ⬆️UUPS Proxies
      • 💡Beacon Proxies
      • 💎Diamond Proxies
  • 🔞Tutorial 19: 18 Decimal Assumption
    • 🚀Prerequisites
    • 📌Understanding ERC20 Decimals
    • 💀Examples Of Vulnerabilities To Do With Assuming 18 Decimals
    • 🤝Conclusion
  • ➗Tutorial 20: Arithmetic
    • 🚀Prerequisites
    • 🕳️Arithmetic pitfall 1: Division by 0
    • 🔪Arithmetic pitfall 2: Precision Loss Due To Rounding
    • 🥸Arithmetic pitfall 3: Erroneous Calculations
    • 🤝Conclusion
  • 🔁Tutorial 21: Unbounded Loops
    • 🚀Prerequisites
    • ⛽Gas Limit Vulnerability
    • 📨Transaction Failures Within Loops
    • 🤝Conclusion
  • 📔Tutorial 22: `isContract`
    • 🚀Prerequisites
    • 💀Understanding the 'isContract()` vulnerability
    • 🤝Conclusion
  • 💵Tutorial 23: Staking
    • 🚀Prerequisites
    • 💀First Depositor Inflation Attack in Staking Contracts
    • 🌪️Front-Running Rebase Attack (Stepwise Jump in Rewards)
    • ♨️Rugability of a Poorly Implemented recoverERC20 Function in Staking Contracts
    • 😠General Considerations for ERC777 Reentrancy Vulnerabilities
    • 🥏Vulnerability: _lpToken and Reward Token Confusion in Staking Contracts
    • 🌊Slippage Checks
    • 🌽The Harvest Functionality in Vaults: Issues and Best Practices
  • ⛓️Tutorial 24: Chain Re-org Vulnerability
    • 🚀Prerequisites
    • ♻️Chain Reorganization (Re-org) Vulnerability
    • 🧑‍⚖️Chain Re-org Vulnerability in Governance Voting Mechanisms
  • 🌉Tutorial 25: Cross Chain Bridges Vulnerabilities
    • 🚀Prerequisites
    • ♻️ERC777 Bridge Vulnerability: Reentrancy Attack in Token Accounting
      • 🛑Vulnerability: Withdrawals Can Be Locked Forever If Recipient Is a Contract
    • 👛The Dangers of Not Using SafeERC20 for Token Transfers
    • Uninitialized Variable Vulnerability in Upgradeable Smart Contracts
    • Unsafe External Calls and Their Vulnerabilities
    • Signature Replay Attacks in Cross-Chain Protocols
  • 🚰Tutorial 26: Integer Underflow and Overflow Vulnerabilities in Solidity (Before 0.8.0)
    • 🚀Prerequisites
    • 💀Understanding Integer Underflow and Overflow Vulnerabilities
    • 🤝Conclusion
  • 🥏Tutorial 27: OpenZeppelin Vulnerabilities
    • 🚀Prerequisites
    • 🛣️A Guide on Vulnerability Awareness and Management
      • 🤝Conclusion
  • 🖊️Tutorial 28: Signature Vulnerabilities / Replays
    • 🚀Prerequisites
    • 🔏Reusing EIP-712 Signatures in Private Sales
    • 🔁Replay Attacks on Failed Transactions
    • 📃Improper Token Validation in Permit Signature
  • 🤝Tutorial 29: Solmate Vulnerabilities
    • 🔏Lack of Code Size Check in Token Transfer Functions in Solmate
  • 🧱Tutorial 30: Inconsistent block lengths across chains
    • 🕛Incorrect Assumptions about Block Number in Multi-Chain Deployments
  • 💉Tutorial 31: NFT JSON and XSS injection
    • 📜Vulnerability: JSON Injection in tokenURI Functions
    • 📍Cross-Site Scripting (XSS) Vulnerability via SVG Construction in Smart Contracts
  • 🍃Tutorial 32: Merkle Leafs
    • 🖥️Misuse of Merkle Leaf Nodes
  • 0️Tutorial 33: Layer 0
    • 📩Lack of Force Resume in LayerZero Integrations
    • ⛽LayerZero-Specific Vulnerabilities in Airdropped Gas and Failure Handling
    • 🔓Understanding the Vulnerability of Blocking LayerZero Channels
    • 🖊️Copy of Understanding the Vulnerability of Blocking LayerZero Channels
  • ♻️Tutorial 34: Forgetting to Update the Global State in Smart Contracts
  • ‼️Tutorial 35: Wrong Function Signature
  • 🛑Tutorial 36: Handling Edge Cases of Banned Addresses in DeFi
  • Tutorial 37: initializer and onlyInitializing
  • ➗Tutorial 38: Eigen Layer
    • 📩Denial of Service in NodeDelegator Due to EigenLayer's maxPerDeposit Check
    • 📈Incorrect Share Issuance Due to Strategy Updates in EigenLayer Integrations
    • 🔁nonReentrant Vulnerability in EigenLayer Withdrawals
  • ⚫Tutorial 39: Wormhole
    • 📩Proposal Execution Failure Due to Guardian Set Change
  • 💼Tutorial 40: Uniswap V3
    • 📩Understanding and Mitigating Partial Swaps in Uniswap V3
    • 🌊Underflow Vulnerability in Uniswap V3 Position Fee Growth Calculations
    • ➗Handling Decimal Discrepancies in Uniswap V3 Price Calculations
  • 🔢Tutorial 41: Multiple Token Addresses in Proxied Tokens
    • 🔓Understanding Vulnerabilities Arising from Tokens with Multiple Entry Points
  • 🤖Tutorial 42: abiDecoder v2
    • 🥥Vulnerabilities from Manipulated Token Interactions Using ABI Decoding
  • ❓Tutorial 43: On-Chain Randomness
    • Vulnerabilities in On-Chain Randomness and How It Can Be Exploited
  • 😖Tutorial 44: Weird ERC20 Tokens
    • Weird Token List
  • 🔨Tutorial 45: Hardcoded stable coin values
  • ❤️Tutorial 46: The Risks of Chainlink Heartbeat Discrepancies in Smart Contracts
  • 👣Tutorial 47: The Risk of Forgetting a Withdrawal Mechanism in Smart Contracts
  • 💻Tutorial 48: Governance and Voting
    • Flash Loan Voting Exploit
    • Exploiting Self-Delegation
    • 💰Missing payable Keyword in Governance Execute Function
    • 👊Voting Multiple Times by Shifting Delegation
    • 🏑Missing Duplicate Veto Check
  • 📕Tutorial 49: Not Conforming To EIP standards
    • 💎Understanding EIP-2981: NFT Royalty Standard
    • 👍Improper Implementation of EIP-2612 Permit Standard
    • 🔁Vulnerabilities of Missing EIP-155 Replay Attack Protection
    • ➡️Vulnerabilities Due to Missing EIP-1967 in Proxy Contracts
    • 🔓Vulnerability of Design Preventing EIP-165 Extensibility
    • 🎟️The Dangers of Not Properly Implementing ERC-4626 in Yield Vaults
    • 🔁EIP-712 Implementation and Replay Attacks
  • ⏳Tutorial 50: Vesting
    • 🚔Vulnerability of Allowing Unauthorized Withdrawals in Vesting Contracts
    • 👊Vulnerability of Unbounded Timelock Loops in Vesting Contracts
    • ⬆️Vulnerability of Incorrect Linear Vesting Calculations
    • ⛳Missing hasStarted Modifier
    • 🔓Vulnerability in Bond Depositor's Vesting Period Reset
  • ⛽Tutorial 51: Ethereum's 63/64 Gas Rule
    • 🛢️Abusing Ethereum's 63/64 Gas Rule to Manipulate Contract Behavior
  • 📩Tutorial 52: NPM Dependency Confusion and Unclaimed Packages
    • 💎Exploiting Unclaimed NPM Packages and Scopes
  • 🎈Tutorial 53: Airdrops
    • 🛄Claiming on Behalf of Other Users
    • 🧲Repeated Airdrop Claims Vulnerability
    • 🍃Airdrop Vulnerability – Merkle Leaves and Parent Node Hash Collisions
  • 🎯Tutorial 54: Precision
    • 🎁Vulnerabilities Due to Insufficient Precision in Reward Calculations
    • Min-Shares: Fixed Minimum Share Values for Tokens with Low Decimal Precision
    • Vulnerability Due to Incorrect Rounding When the Numerator is Not a Multiple of the Denominator
    • Vulnerability from Small Deposits Being Rounded Down to Zero Shares in Smart Contracts
    • Precision Loss During Withdrawals from Vaults Can Block Token Transfers Due to Value < Amount
    • 18 Decimal Assumption Scaling: Loss of Precision in Asset Conversion Due to Incorrect Scaling
  • Tutorial 55: AssetIn == AssetOut, FromToken == ToToken
    • 🖼️Vulnerability: Missing fromToken != toToken Check
  • 🚿Tutorial 56: Vulnerabilities Related to LP Tokens Being the Same as Reward Tokens
    • 🖼️Vulnerabilities Caused by LP Tokens Being the Same as Reward Tokens
  • Tutorial 57: Unsanitized SWAP Paths and Arbitrary Contract Call Vulnerabilities
    • 📲Arbitrary Contract Calls from Unsanitized Paths
  • Tutorial 58: The Risk of Infinite Approvals and Arbitrary Contract Calls
    • 🪣Exploiting Infinite Approvals and Arbitrary Contract Calls
  • Tutorial 59: Low-Level Calls in Solidity Returning True for Non-Existent Contracts
    • Low-Level Calls Returning True for Non-Existent Contracts
  • 0️⃣Tutorial 60: The Impact of PUSH0 and the Shanghai Hardfork on Cross-Chain Deployments > 0.8.20
    • PUSH0 and Cross-Chain Compatibility Challenges
  • 🐍Tutorial 61: Vyper Vulnerable Versions
    • Vyper and the EVM
  • ⌨️Tutorial 62: Typos in Smart Contracts — The Silent Threat Leading to Interface Mismatch
    • Vyper and the EVM
  • ☁️Tutorial 63: Balance Check Using ==
    • The Vulnerability: == Balance Check
  • 💍Tutorial 64: Equal Royalties for Unequal NFTs
    • Understanding the Problem: Equal Royalties for Unequal NFTs
  • 🖼️Tutorial 65: ERC721 and NFTs
    • The Risk of Using transferFrom Instead of safeTransferFrom in ERC721 Projects
    • ❄️Why _safeMint Should Be Used Instead of _mint in ERC721 Projects
    • The Importance of Validating Token Types in Smart Contracts
    • 📬Implementing ERC721TokenReceiver to Handle ERC721 Safe Transfers
    • NFT Implementation Deviating from ERC721 Standard in Transfer Functions
    • NFT Approval Persistence after Transfer
    • 🎮Gameable NFT Launches through Pseudo-Randomness
    • 2️⃣Protecting Buyers from Losing Funds Due to Claimed NFT Rewards on Secondary Markets
    • ♻️Preventing Reentrancy When Using SafeERC721
    • 🖊️Preventing Re-use of EIP-712 Signatures in NFT Private Sales
  • 2️⃣Tutorial 66: Vulnerability Arising from NFTs Supporting Both ERC721 and ERC1155 Standards
  • 📷Tutorial 67: ERC1155 Vulnerabilities
    • ♻️Preventing Reentrancy in OpenZeppelin's SafeERC1155
    • 🛫Vulnerabilities in OpenZeppelin's ERC1155Supply Contract
    • Understanding Incorrect Token Owner Enumeration in ERC1155Enumerable
    • Avoiding Breaking ERC1155 Composability with Improper safeTransferFrom Implementation
    • 💍Ensuring Compatibility with EIP-2981 in ERC1155 Contracts
  • 🪟Informational Vulnerabilities
  • ⛽Gas Efficiency
  • 💻Automation Tools
  • 🔜Out Of Gas (Coming Soon)
  • 🔜DEX Aggregators (Coming Soon)
  • 🔜Bribes (Coming Soon)
  • 🔜Understanding Compiled Bytecode (coming soon)
  • 🔜Deployment Mistakes (coming soon)
  • 🔜Optimistic Roll-ups (coming soon)
  • 🔜Typos (coming soon)
  • 🔜Try-Catch (coming soon)
  • 🔜NFT Market-place (coming soon)
  • 🔜Upgrade-able Contracts (coming soon)
Powered by GitBook
On this page
  1. Tutorial 13: Liquid Staking

Example Vulnerability

PreviousUnderstanding Liquid Staking VulnerabilitiesNextExample Vulnerability 2

Last updated 1 year ago

Incorrect assumpiton of Ethereum client implementation

Bug bounty payouts

  1. lido: $100k

  2. rocket finance $100k

  3. Tranches: 44.8 eth

Current TVL of effected projects at the time of writing:

  1. $15.03 Billion

  2. $1.89 Billion

  3. $10.88 Million

Refrences and further reading links:

In the ever-evolving world of blockchain, liquid staking has emerged as a transformative solution for participants unable to stake the required 32 ETH in the Ethereum 2.0 network. As a significant upgrade, Ethereum 2.0 is designed not only to enhance transaction processing capabilities but also to introduce improved security and scalability. Transitioning from Proof of Work (PoW) to Proof of Stake (PoS), Ethereum 2.0 ditches miners for validators. To become one, participants need to stake a significant 32 ETH, which given the price of Ether can be prohibitively expensive for many.

Liquid staking represents an innovative approach to traditional staking mechanisms on prominent blockchain networks. Rather than merely staking assets and waiting for returns, liquid staking introduces a layer of flexibility and liquidity that benefits users in multiple ways.

Users can stake their assets, contributing to the network's stability, and in return, they earn rewards. But what sets liquid staking apart is its ability to tokenize these staked assets. By turning them into tokens of equivalent value, users gain the ability to move, trade, or use these tokens as they wish, all while continuing to earn staking rewards.

Here's a step-by-step of how the process typically unfolds:

  1. Users initiate the process by depositing their assets into the liquid staking platform.

  2. The platform, in turn, pools these deposits and stakes these assets on the ethereum network to generate rewards.

  3. A portion of the rewards generated is allocated to users in proportion to their deposits.

  4. In parallel, the deposited assets are tokenized into a specific type of token, often compliant with popular standards like ERC-20. This token essentially represents a user's staked assets in a liquid form.

  5. Users can store, trade, or use this token like any other digital asset in their portfolio, offering unprecedented flexibility.

  6. Whenever users desire, they can convert these tokens back to their original assets.

In essence, liquid staking seamlessly merges the benefits of staking for network stability and rewards with the advantages of tokenized assets' liquidity.

Unmasking the Attack Vector

The upcoming vulnerability discussion stems from inherent structural issues between Liquid Staking Platforms (LSP) and outsourced node operators, coupled with certain challenges in the Ethereum 2.0 Beacon client.

To truly grasp the depth of this vulnerability, we must first acquaint ourselves with the inner workings of a typical Liquid Staking Platform.

Initially, like many, I was under the impression that the core team behind these platforms directly operated nodes. This understanding, however, proved to be a misconception. Instead of direct node operation, many LSP teams delegate this crucial task to third-party entities.

This operational model isn't exclusive to one platform; even some of the leading competitors in the liquid staking space follow a similar structure, entrusting third parties with node operations.

For a more detailed partnership example within the industry, you can refer to the relevant partnership announcements made by these platforms.

Let's delve into the architecture of the Ethereum 2.0 Beacon client.

As Ethereum evolves to its 2.0 version, it's pivoting from the Proof-of-Work (PoW) consensus to the Proof-of-Stake (PoS) mechanism. Integral to this transition is the introduction of an official Deposit Contract, designated by the address: 0x00000000219ab540356cBB839Cbe05303d7705Fa.

Below, you'll find the official code for this Deposit contract.

// https://github.com/prysmaticlabs/prysm/blob/develop/contracts/deposit/deposit_contract.sol#L101-L159
function deposit(
    bytes calldata pubkey,
    bytes calldata withdrawal_credentials,
    bytes calldata signature,
    bytes32 deposit_data_root
) override external payable {
    // Extended ABI length checks since dynamic types are used.
    require(pubkey.length == 48, "DepositContract: invalid pubkey length");
    require(withdrawal_credentials.length == 32, "DepositContract: invalid withdrawal_credentials length");
    require(signature.length == 96, "DepositContract: invalid signature length");

    // Check deposit amount
    require(msg.value >= 1 ether, "DepositContract: deposit value too low");
    require(msg.value % 1 gwei == 0, "DepositContract: deposit value not multiple of gwei");
    uint deposit_amount = msg.value / 1 gwei;
    require(deposit_amount <= type(uint64).max, "DepositContract: deposit value too high");

    // Emit `DepositEvent` log
    bytes memory amount = to_little_endian_64(uint64(deposit_amount));
    emit DepositEvent(
        pubkey,
        withdrawal_credentials,
        amount,
        signature,
        to_little_endian_64(uint64(deposit_count))
    );

    // Compute deposit data root (`DepositData` hash tree root)
    bytes32 pubkey_root = sha256(abi.encodePacked(pubkey, bytes16(0)));
    bytes32 signature_root = sha256(abi.encodePacked(
            sha256(abi.encodePacked(signature[:64])),
            sha256(abi.encodePacked(signature[64:], bytes32(0)))
        ));
    bytes32 node = sha256(abi.encodePacked(
            sha256(abi.encodePacked(pubkey_root, withdrawal_credentials)),
            sha256(abi.encodePacked(amount, bytes24(0), signature_root))
        ));

    // Verify computed and expected deposit data roots match
    require(node == deposit_data_root, "DepositContract: reconstructed DepositData does not match supplied deposit_data_root");

    // Avoid overflowing the Merkle tree (and prevent edge case in computing `branch`)
    require(deposit_count < MAX_DEPOSIT_COUNT, "DepositContract: merkle tree full");

    // Add deposit data root to Merkle tree (update a single `branch` node)
    deposit_count += 1;
    uint size = deposit_count;
    for (uint height = 0; height < DEPOSIT_CONTRACT_TREE_DEPTH; height++) {
        if ((size & 1) == 1) {
            branch[height] = node;
            return;
        }
        node = sha256(abi.encodePacked(branch[height], node));
        size /= 2;
    }
    // As the loop should always end prematurely with the `return` statement,
    // this code should be unreachable. We assert `false` just to be safe.
    assert(false);
}

Once the deposit function is invoked, using parameters like pubkey, withdrawal_credentials, signature, and deposit_data_root, and the deposit finalizes, a DepositEvent is triggered.

This event prompts Ethereum 2.0 Beacon clients, including the likes of Prysm and Lighthouse, to refresh their staking states.

Below is the code implementation for the Prysm client.

// https://github.com/prysmaticlabs/prysm/blob/797cc360c77c650751499e375da2c96be9b32b69/beacon-chain/execution/log_processing.go#L89-L106
// ProcessLog is the main method which handles the processing of all
// logs from the deposit contract on the eth1 chain.
func (s *Service) ProcessLog(ctx context.Context, depositLog gethtypes.Log) error {
    s.processingLock.RLock()
    defer s.processingLock.RUnlock()
    // Process logs according to their event signature.
    if depositLog.Topics[0] == depositEventSignature {
        if err := s.ProcessDepositLog(ctx, depositLog); err != nil {
            return errors.Wrap(err, "Could not process deposit log")
        }
        if s.lastReceivedMerkleIndex%eth1DataSavingInterval == 0 {
            return s.savePowchainData(ctx)
        }
        return nil
    }
    log.WithField("signature", fmt.Sprintf("%#x", depositLog.Topics[0])).Debug("Not a valid event signature")
    return nil
}
...
// https://github.com/prysmaticlabs/prysm/blob/797cc360c77c650751499e375da2c96be9b32b69/beacon-chain/execution/log_processing.go#L108-L225
func (s *Service) ProcessDepositLog(ctx context.Context, depositLog gethtypes.Log) error {
    pubkey, withdrawalCredentials, amount, signature, merkleTreeIndex, err := contracts.UnpackDepositLogData(depositLog.Data)
    if err != nil {
        return errors.Wrap(err, "Could not unpack log")
    }
    // If we have already seen this Merkle index, skip processing the log.
    // This can happen sometimes when we receive the same log twice from the
    // ETH1.0 network, and prevents us from updating our trie
    // with the same log twice, causing an inconsistent state root.
    index := int64(binary.LittleEndian.Uint64(merkleTreeIndex)) // lint:ignore uintcast -- MerkleTreeIndex should not exceed int64 in your lifetime.
    if index <= s.lastReceivedMerkleIndex {
        return nil
    }

    if index != s.lastReceivedMerkleIndex+1 {
        missedDepositLogsCount.Inc()
        return errors.Errorf("received incorrect merkle index: wanted %d but got %d", s.lastReceivedMerkleIndex+1, index)
    }
    s.lastReceivedMerkleIndex = index

    // We then decode the deposit input in order to create a deposit object
    // we can store in our persistent DB.
    depositData := &ethpb.Deposit_Data{
        Amount:                bytesutil.FromBytes8(amount),
        PublicKey:             pubkey,
        Signature:             signature,
        WithdrawalCredentials: withdrawalCredentials,
    }

    depositHash, err := depositData.HashTreeRoot()
    if err != nil {
        return errors.Wrap(err, "unable to determine hashed value of deposit")
    }

    // Defensive check to validate incoming index.
    if s.depositTrie.NumOfItems() != int(index) {
        return errors.Errorf("invalid deposit index received: wanted %d but got %d", s.depositTrie.NumOfItems(), index)
    }
    if err = s.depositTrie.Insert(depositHash[:], int(index)); err != nil {
        return err
    }

    deposit := &ethpb.Deposit{
        Data: depositData,
    }
    // Only generate the proofs during pre-genesis.
    if !s.chainStartData.Chainstarted {
        proof, err := s.depositTrie.MerkleProof(int(index))
        if err != nil {
            return errors.Wrap(err, "unable to generate merkle proof for deposit")
        }
        deposit.Proof = proof
    }

    // We always store all historical deposits in the DB.
    root, err := s.depositTrie.HashTreeRoot()
    if err != nil {
        return errors.Wrap(err, "unable to determine root of deposit trie")
    }
    err = s.cfg.depositCache.InsertDeposit(ctx, deposit, depositLog.BlockNumber, index, root)
    if err != nil {
        return errors.Wrap(err, "unable to insert deposit into cache")
    }
    validData := true
    if !s.chainStartData.Chainstarted {
        s.chainStartData.ChainstartDeposits = append(s.chainStartData.ChainstartDeposits, deposit)
        root, err := s.depositTrie.HashTreeRoot()
        if err != nil {
            return errors.Wrap(err, "unable to determine root of deposit trie")
        }
        eth1Data := &ethpb.Eth1Data{
            DepositRoot:  root[:],
            DepositCount: uint64(len(s.chainStartData.ChainstartDeposits)),
        }
        if err := s.processDeposit(ctx, eth1Data, deposit); err != nil {
            log.WithError(err).Error("Invalid deposit processed")
            validData = false
        }
    } else {
        root, err := s.depositTrie.HashTreeRoot()
        if err != nil {
            return errors.Wrap(err, "unable to determine root of deposit trie")
        }
        s.cfg.depositCache.InsertPendingDeposit(ctx, deposit, depositLog.BlockNumber, index, root)

Upon examining the section that manages the Deposit Event, it's clear that the balance in the Beacon state, corresponding to the initially set Withdrawal credential for a specific pubkey, continues to augment. This indicates that the first-set Withdrawal credential remains persistent and dominant, even if there are subsequent attempts to alter it.

Vulnerability Overview: A malicious third-party node operator can exploit the Ethereum 2.0 Beacon client's Deposit Event handling mechanism. The attack revolves around "frontrunning" legitimate deposit transactions to manipulate the withdrawal credentials for the staked Ether.

Attack Breakdown:

  1. Initiation: A rogue validator, intending to exploit the system, prepares malicious deposit data.

  2. Frontrunning: Before a legitimate deposit transaction of 32 ETH takes place, the malicious node operator "frontruns" it by executing their prepared transaction.

  3. Malicious Deposit Data: This malicious transaction utilizes the same validator's public key (pubkey) as the upcoming legitimate deposit. However, it only deposits a minimum required amount (e.g., 1 ETH or 16 ETH in the case of certain platforms like RocketPool). Crucially, the withdrawal credential in this transaction is different from the one originally agreed upon with the protocol.

  4. System Behavior Exploitation: Due to how the Beacon client operates, if a pubkey hasn't been previously registered, the system would prioritize and process this new malicious deposit first, believing it to be the first time this validator's key is seen. As a result, when the genuine 32 ETH deposit transaction follows, the Beacon client simply adds this amount to the malicious operator's deposit.

  5. Outcome: The net result is that the staked 32 ETH now has the withdrawal credentials set by the attacker. Essentially, the malicious operator can now control and potentially withdraw these funds.

Impact: By exploiting this vulnerability, the malicious node operator can effectively divert and control staked assets meant for a legitimate node. This means the funds from unsuspecting pool users, expecting to stake under a set withdrawal credential, are now under the control of the attacker.

Mitigation Steps for Frontrunning Attack in Ethereum 2.0 Staking Contracts:

  1. Verification of Node Operators' Keys:

    • Contracts must have the capability to verify that Node Operators' keys have not been used for any malicious pre-deposits. This can be done by cross-referencing with a predefined list of authorized keys.

  2. Establish a Deposit Security Committee:

    • Form a committee responsible for monitoring all deposit activities.

    • Their role includes validating the current state of deposits and affirming that available staking keys are safe for use.

  3. Use Merkle Root for Key Verification:

    • The staking contract (like Lido in the example) should fetch the Merkle root encoding of all keys used in staking deposits.

    • This Merkle root data will act as a point of reference to verify the authenticity of staking keys.

  4. Off-Chain Guardian Committee Verification:

    • To conserve gas costs, the Merkle root data should be checked and signed by an off-chain guardian committee. This committee will ensure that the data is accurate and untampered.

  5. Enforce Secure Deposit Transaction:

    • Implement a function named depositBufferedEther. This function ensures that buffered ether can only be sent to the staking contract if the following conditions are met:

      • The Merkle root data from the staking contract matches the on-chain Merkle root data.

      • Staking hasn't been paused.

      • A quorum of guardians has provided their signatures, ensuring the transaction's legitimacy.

      • There's a minimum block distance between deposits to prevent rapid, frequent deposits.

      • The block hash provided matches the expected hash for the given block number.

      • The staking keys operation index has not been tampered with.

  6. Transaction Verification:

    • The provided signatures should be verified against the staking contract's current state. If all conditions are met, the deposit transaction is approved and executed.

    • Otherwise, if any data point changes (due to a deposit, modification of approved staking keys, or a blockchain reorganization) before the deposit transaction, the staking contract will reject the transaction.

  7. Routine Updating of Deposit Transactions:

    • Regularly update the block number of the last deposit to monitor deposit frequency and maintain security measures.

By following these steps, staking platforms can significantly minimize the risk associated with the frontrunning attack, ensuring the integrity of staking operations.

(If you haven't read our article on front running you can find it )

🌊
🛑
here
Book an audit with Zokyo
LogoRocketPool and Lido Frontrunning Bugfix ReviewMedium
LogoCode4renaCode4rena
LogoRecap: Deposit Front-run Vulnerability MitigationMedium