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
  • Example 1: OpenSea -> Merkle Tree criteria can be resolved by wrong tokenIDs
  • Example 2: Nomad Bridge -> Bad Merkle Proofs - Included 0x0 address as a root
  • Example 3: Duplicate Claim Exploit Found in Balancer's Merkle Orchard: Understanding the Vulnerability
  • Example 4: Binance Bridge -> flaw in the IAVL Merkle proof verification system
  1. Tutorial 6: Merkle Trees

Example Vulnerabilities

PreviousVulnerabilities When Using Merkle TreesNextExercise

Last updated 1 year ago

In the previous section, we discussed potential vulnerabilities that might occur due to incorrect or poor implementation of Merkle Trees in cryptographic systems. While the theory of Merkle Trees is sound, in practice, there have been instances where these vulnerabilities have been exposed, leading to security issues. In this section, we will delve into real-life scenarios where vulnerabilities arose due to improper use or implementation of Merkle Trees. These case studies will underscore the importance of secure design principles when using Merkle Trees and illuminate common pitfalls that developers should be wary of.

Example 1: OpenSea -> Merkle Tree criteria can be resolved by wrong tokenIDs

This vulnerability, at its core, arises from the mishandling of leaves in the Merkle tree construction within the smart contract. To give some context, a leaf in a Merkle tree represents the data — in this case, the tokenId — being included in the tree.

Looking at the provided function _verifyProof, the issue is that the tokenIds are directly used as the leaves of the tree, not their hashes. In other words, the actual data (the tokenId) isn't being hashed to form the leaves. Consequently, any intermediate hash value from the Merkle tree — not just the leaves — can be used to fulfil the trade, which means that a token the offerer did not specify could be used.

Let's consider an example. Alice wishes to buy an NFT with either tokenId 1 or tokenId 2. She constructs a Merkle tree with these tokenIds, and the root becomes the hash of the concatenation of 1 and 2. Alice then creates an offer using this root as the criterion.

An attacker could, however, fulfill the trade using any NFT with a tokenId that happens to be an intermediate hash value from the Merkle tree. Consequently, Alice receives a token that doesn't meet her criteria, leading to losses.

This vulnerability exploits the standard behavior of ERC-721 tokens, which allows the tokenId to be an arbitrary number, not necessarily a simple counter. ERC-721 treats the tokenId as a "black box," and doesn't require any specific pattern.

The key to resolving this vulnerability lies in hashing the tokenId to form the leaf nodes of the Merkle tree, not the tokenId itself. This can be achieved by replacing let computedHash := leaf with bytes32 computedHash = keccak256(abi.encodePacked(leaf)), thus hashing the tokenIds to form the leaves. This simple yet crucial modification ensures there can't be any collision between a leaf hash and an intermediate hash, as they result from hashing different byte lengths. Note that this fix would require changes to the off-chain process of how the Merkle tree is generated, as leaves must be hashed first.

function _verifyProof(
    uint256 leaf,
    uint256 root,
    bytes32[] memory proof
) internal pure {
    bool isValid;

-    assembly { //remove
-        let computedHash := leaf //remove
+  bytes32 computedHash = keccak256(abi.encodePacked(leaf))//add
  ...

In conclusion, this case exemplifies how a simple oversight in the implementation of a Merkle tree can lead to serious vulnerabilities, underscoring the importance of proper handling and processing of leaves in the tree structure.

Example 2: Nomad Bridge -> Bad Merkle Proofs - Included 0x0 address as a root

Understanding How Mismanagement of Trusted Roots and EVM Storage Defaults Led to a $190m DeFi Hack

The August 2022 exploit on the Nomad bridge was centered around a smart contract code vulnerability within the Replica contract. The process function in this contract is responsible for dispatching a message to its final recipient, and this can only be successful if the message has already been proven, i.e., it has been added to the Merkle tree leading to an accepted and trustworthy root.

function process(bytes memory _message) public returns (bool _success) {
       ...
       bytes32 _messageHash = _m.keccak();
       require(acceptableRoot(messages[_messageHash]), "!proven");
       ...
}

This message check is done against the message hash using the acceptableRoot view function, which reads from the confirmed roots mapping.

During a routine upgrade in April 21st, a zero value was passed as the pre-approved committed root, which was stored into the confirmAt mapping.

function initialize(
       ...
       bytes32 _committedRoot,
       ...
   ) public initializer {
       ...
       // pre-approve the committed root.
       confirmAt[_committedRoot] = 1;
       ...
   }

This zero value was the source of the vulnerability. In the Ethereum Virtual Machine (EVM), all storage slots are initialized as zero values. This means that any unused key on a Solidity mapping will return 0x00. Therefore, when the message hash is not present in the messages mapping, 0x00 is returned, and that is passed to the acceptableRoot function.

As a result, if 0x00 has been set as a trusted root, the acceptableRoot function will return true, marking the message as processed. However, an attacker can simply change the message to create a new unused one and resubmit it.

When an attacker submits a crafted message to the process() function, a hash of the message (_messageHash) is created using the keccak() function. This hash is then used as a key to look up a corresponding value in the messages mapping.

In an Ethereum smart contract, any uninitialized storage slot, including those in mappings, will default to a value of 0x0 (a 'zero-hash'). Thus, if a hash key is not found in the messages mapping (which will be the case for a new, unique message submitted by an attacker), the default return value will be 0x0.

The acceptableRoot() function then checks if this returned value (0x0) is a 'trusted root' in the confirmAt mapping. Due to the misconfiguration during the initialization of the Replica contract, 0x0 had been inadvertently marked as a trusted root.

This means the requirement require(acceptableRoot(messages[_messageHash]), "!proven") will pass, even for the attacker's unique, unproven message, as the acceptableRoot() function erroneously returns true when it encounters the default 'zero-hash' value. As a result, the attacker's message can be processed and the funds can be unlocked, leading to the exploitation.

Among the parameters encoded in the input message is the recipient address. After the first successful transaction, anyone who understood the message format could alter the recipient address and replay the attack transaction, this time with a different message that would yield profit to a new address.

This replay-ability, coupled with the faulty zero value initialization, allowed multiple attackers to drain the funds from the bridge.

Here is the full code of the process function:

   function process(bytes memory _message) public returns (bool _success) {
       // ensure message was meant for this domain
       bytes29 _m = _message.ref(0);
       require(_m.destination() == localDomain, "!destination");
       // ensure message has been proven
       bytes32 _messageHash = _m.keccak();
       require(acceptableRoot(messages[_messageHash]), "!proven");
       // check re-entrancy guard
       require(entered == 1, "!reentrant");
       entered = 0;
       // update message status as processed
       messages[_messageHash] = LEGACY_STATUS_PROCESSED;
       // call handle function
       IMessageRecipient(_m.recipientAddress()).handle(
           _m.origin(),
           _m.nonce(),
           _m.sender(),
           _m.body().clone()
       );
       // emit process results
       emit Process(_messageHash, true, "");
       // reset re-entrancy guard
       entered = 1;
       // return true
       return true;
   }

here is the full code of the initialize function

 function initialize(
       uint32 _remoteDomain,
       address _updater,
       bytes32 _committedRoot,
       uint256 _optimisticSeconds
   ) public initializer {
       __NomadBase_initialize(_updater);
       // set storage variables
       entered = 1;
       remoteDomain = _remoteDomain;
       committedRoot = _committedRoot;
       // pre-approve the committed root.
       confirmAt[_committedRoot] = 1;
       _setOptimisticTimeout(_optimisticSeconds);
   }

Example 3: Duplicate Claim Exploit Found in Balancer's Merkle Orchard: Understanding the Vulnerability

A deep-dive into the high-severity flaw that allowed liquidity providers to submit duplicate claims and potentially drain assets from the Balancer's Vault.

On January 22, 2023, the Balancer protocol, a renowned decentralized finance (DeFi) liquidity infrastructure provider, was alerted to a high-severity vulnerability by the white-hat hacker known as 0xriptide. This bug, if exploited, would have allowed liquidity providers to submit duplicate claims and drain the Merkle Orchard's assets from the Balancer's Vault. The vulnerability could have impacted around $3.2 million worth of funds across Ethereum mainnet, Polygon, and Arbitrum.

Despite the Merkle Orchard contract not being within the bug bounty program's scope, Balancer awarded 0xriptide a 50 ETH bounty, appreciating the importance of the finding. This demonstrates the effectiveness of Balancer's well-run bounty program with fast response times and attractive rewards, leading to such funds-saving discoveries.

The Vulnerability Breakdown

The bug was nestled in the Merkle Orchard contracts, implemented in late 2021. The main purpose of these contracts was to enable liquidity providers to claim reward distributions of multiple tokens in a single transaction. This was accomplished via calling the MerkleOrchard.claimDistributions function, which internally invokes the _processClaims function.

function _processClaims(
       address claimer,
       address recipient,
       Claim[] memory claims,
       IERC20[] memory tokens,
       bool asInternalBalance
   ) internal { /*...*/ }

This function processes an array of claims. Each claim has a distributionId, from which _getIndices computes a word index and a bit index. In parallel, the _getChannelId function calculates the channel id.

 function _getChannelId(IERC20 token, address distributor) private pure returns (bytes32) {
       return keccak256(abi.encodePacked(token, distributor));
   }

If there are duplicate claims in the array, they produce the same channel id. Consequently, the function bypasses the _setClaimedBits call. This function sets bits in a bitmap to track the committed claims and prevents duplicated transactions. Therefore, duplicate claims within the array would accumulate currentClaimAmount without any bitmap checks.

if (currentChannelId == _getChannelId(tokens[claim.tokenIndex], claim.distributor)) {
   if (currentWordIndex == distributionWordIndex) {
       currentBits |= 1 << distributionBitIndex;
   } else {
       _setClaimedBits(currentChannelId, claimer, currentWordIndex, currentBits);
   }
   currentClaimAmount += claim.balance;
} else { /*...*/ }

The function only calls _setClaimedBits when it reaches the final element of the array, so the claim still needs to be valid and must provide a corresponding Merkle proof. At the end, the function calls manageUserBalance on Balancer’s Vault contract to send the claimed funds to the recipient.

if (i == claims.length - 1) {
    _setClaimedBits(currentChannelId, claimer, currentWordIndex, currentBits);
    _deductClaimedBalance(currentChannelId, currentClaimAmount);
}

/* ... */

IVault.UserBalanceOpKind kind = asInternalBalance
    ? IVault.UserBalanceOpKind.TRANSFER_INTERNAL
    : IVault.UserBalanceOpKind.WITHDRAW_INTERNAL;
IVault.UserBalanceOp[] memory ops = new IVault.UserBalanceOp[](tokens.length);
/* ... */
getVault().manageUserBalance(ops);

Example 4: Binance Bridge -> flaw in the IAVL Merkle proof verification system

This flaw allowed a malicious hacker to steal 2m BNB, or about $600m USD equivalent at the time.

On October 7th, 2022, the Binance Bridge was hacked due to a flaw in the IAVL Merkle proof verification system, which allowed a malicious hacker to steal 2m BNB, or about $600m USD equivalent at the time.

Binance is a centralized exchange (CEX) that offers a wide range of services. It has two chains under its name: BNB Beacon Chain (BEP), which is used for governance, and BNB Chain (BNB), an L1 blockchain that is compatible with EVM and allows anyone to deploy and execute smart contracts on its chain.

BSC Token Hub is the bridge between BNB Beacon Chain (BEP2) and BNB Chain (BNB). It interacts with the Cross Chain contract, which allows a relayer to forward the user’s token to BSC from BEP2. It verifies the token transfer through an IAVL verification, which can be validated from payload, proofs, package sequence, height, and channel id. And if the verification is deemed successful, the token would be transferred to the user in BSC.

Merkle tree

With a Merkle tree structure, we can verify if data is included in a database without disclosing all of the data in that database by hashing its value and comparing it against the root hash of the Merkle tree.

The data in each node in the Merkle tree is a hash of each child concatenated value. If it’s a leaf node, the data is the hash value of the underlying data. So, the data in P1 is the hash of P3, and P4 (i.e P1=h(P3, P4)), and the data in P3 is the hash value of the data (i.e P3=h(data)).

To be able to verify the data against the Merkle tree, we must first know what’s the path node of the data that we want to verify. The path node is nodes that lie on the path from the leaf node to the root node of the Merkle tree. The path node essentially is a guidance node that will bring our data to reach its root node.

For example, suppose we have some data in P3, and we want to verify whether this data is included in the Merkle tree or not. First, we must hash our data to generate the P3 value. Then, we must determine the path node of P3. In this case, the path node for P3 is P4 and P2. So by providing only the P4 and P2 values, we can reach the root node. The value for P4 and P2 can be considered as the Merkle proofs of the data we have in P3. And these proofs can be used to verify the data in P3.

You might ask: why is P1 not included in the path node? This is because we can generate the P1 value ourselves by concatenating the value of P3 and P4, which is equal to the value of P1.

Naturally, one character difference in hashing will produce a completely different output. Therefore, the order of the concatenation of the node to generate the P1 or any non-leaf node is crucial. And there are multiple ways to do this ordering. In this case, we will cover the common ordering in the Merkle tree and the ordering that was used by Binance, which unfortunately led to the exploit.

Common Ordering

The common concatenation ordering for Merkle verification in the Merkle tree is done by comparing the values of its child node. If the value of the child node is bigger than the value of the other child, then it’s being concatenated to the child node that has a bigger value.

For example, node P1 has a P3 and P4 node as its child node. To get the value of P1, we must concat the P3 and P4 values and then hash the concatenated value. If the P3 value is bigger than the P4, then the ordering is (P3, P4), and if the P4 value is bigger than the P3, then the ordering is (P4, P3)

Ordering in the IAVL verification:

IAVL verification does its concatenation ordering by adding additional attributes to its path nodes. These attributes are right and left attributes. This was done to determine whether it should be hashed to the left or right side of the node.

For example, we can look at node P4 which has left attributes set to null and right attributes set to P4. This means that we can get the value P1 by hashing its child value with the P4 value as the right side of the data, or P1=H(P3, P4).

To consolidate:

IAVL trees are different from standard Merkle trees in that they have attributes assigned to their nodes, called "right" and "left". These attributes are used to determine how to concatenate and hash node values. If the node has a "right" attribute, it is hashed to the right side of its child node value. For example, if node P4 has its "right" attribute set to P4 and "left" attribute set to null, to obtain the value of P1, you hash the value of child node P3 with the value of P4 as the right-side data.

The exploit described here is based on the fact that user-controlled "proof" data was not correctly validating the "right" attribute of a node when calculating the root hash of the tree. Essentially, the "right" attribute was not being properly checked or validated, allowing an attacker to provide a malicious "right" attribute. Because the verification code did not properly check this value, the malicious proof was accepted as valid.

But how does faking the right side give a valid root?

In the case of this exploit, it seems that the attacker was able to manipulate the proof in such a way that the calculated root from the malicious proof matched the expected root, even though the malicious data was inserted into the proof.

Remember, the proof is essentially a series of hashes and metadata that allow anyone to verify that a specific piece of data is part of the tree without seeing the entire tree.

In a correctly functioning system, any change to the data (or the proof) should cause the calculated root to differ from the expected root, making the proof invalid.

However, the bug in the IAVL tree was that it did not properly verify the "right" attribute during the proof verification. The "right" attribute specifies which side (left or right) a node should be hashed with its sibling.

In this exploit, the attacker manipulated the proof's "right" attribute to insert their malicious payload into the proof. Because of the bug, this manipulation wasn't caught during verification, and the calculated root from the manipulated proof ended up matching the expected root, thus making the proof appear valid.

So in this case both the root has and the right side of the tree isn't being verified?

Yes, according to the vulnerability discussed in the patent, it seems both the right side of the tree and the root hash are not being properly validated in this flawed implementation.

Normally, the right side should be verified. In a correctly implemented Merkle tree, each leaf node (transaction data) is hashed and then these hashes are paired and hashed again. This process is repeated up to the root node. If any of the transaction data is changed, it would change the hash of that leaf node, which would then change the hash of the parent node, and so on, up to the root node. Therefore, any change in the tree (left or right) should cause the root hash to change.

Moreover, the root hash should also be verified. It's the ultimate proof of the integrity of all the transaction data in the tree. If all data is unchanged, the root hash should match the expected value. If any data is tampered with, the root hash would change, and this change would signal the tampering.

However, according to the patent, in this flawed implementation, the system isn't checking the right side or recalculating and validating the root hash after the left side is verified. This means an attacker can substitute a different right half of the tree, and the system doesn't notice because it isn't checking the right half or the root hash again. The system continues to operate under the assumption that the root hash is still valid, even though the tree has been tampered with.

Please refer to this article for a full proof of concept:

The root cause of this exploit is that the right attribute of the node was not used to calculate the root hash of the tree. Since the , the hacker was able to pass the malicious right attributes, which is the hash of the malicious payload, and the proof would be considered valid.

🌴
💀
Book an audit with Zokyo
Code4rena Bounty
proof was user-controllable
LogoRekt - Nomad Bridge - REKTrekt
LogoBalancer Logic Error Bugfix ReviewMedium
LogoHack Analysis: Binance Bridge, October 2022Medium
LogoHack Analysis: Binance Bridge, October 2022Medium