Overview
TheMerkleLib library provides utility functions for merkle tree verification and claim tracking in the Across Protocol. It enables efficient validation of merkle proofs for pool rebalances, relayer refunds, and slow fills, as well as bitmap-based tracking of executed leaves.
Location: contracts/MerkleLib.sol
Purpose
MerkleLib serves two primary functions:- Merkle Proof Verification: Validates that specific leaves (rebalances, refunds, fills) are included in proposed merkle roots
- Claim Tracking: Uses bitmap patterns to efficiently track which merkle leaves have been executed, preventing double-execution
Merkle Proof Functions
These functions verify that specific data structures are contained within merkle trees proposed by data workers.verifyPoolRebalance
Verifies that a pool rebalance leaf is included in a merkle root.root: The merkle root to verify againstrebalance: The pool rebalance struct to verifyproof: Array of merkle proof hashes
true if the rebalance is contained in the tree, false otherwise
Implementation:
verifyRelayerRefund
Verifies that a relayer refund leaf is included in a merkle root.root: The merkle root to verify againstrefund: The relayer refund struct to verifyproof: Array of merkle proof hashes
true if the refund is contained in the tree, false otherwise
Implementation:
verifyV3SlowRelayFulfillment
Verifies that a V3 slow fill leaf is included in a merkle root.root: The merkle root to verify againstslowRelayFulfillment: The slow fill struct to verifyproof: Array of merkle proof hashes
true if the slow fill is contained in the tree, false otherwise
Implementation:
Claim Tracking Functions
These functions implement efficient bitmap-based tracking of executed merkle leaves, preventing double-execution attacks.2D Bitmap Functions (Storage-Based)
Used for tracking claims across multiple root bundles.isClaimed
Checks if a specific index has been claimed in a 2D bitmap.claimedBitMap: Storage mapping used as a 2D bitmapindex: The index to check
true if the index has been claimed, false otherwise
Implementation:
- Divides the index space into 256-bit “words”
- Each word is stored as a
uint256in the mapping - Within each word, individual bits represent claim status
setClaimed
Marks a specific index as claimed in a 2D bitmap.claimedBitMap: Storage mapping used as a 2D bitmapindex: The index to mark as claimed
1D Bitmap Functions (In-Memory)
Used for tracking claims within a single uint256 value (up to 256 claims).isClaimed1D
Checks if a specific index has been claimed in a 1D bitmap.claimedBitMap: Auint256value encoding a 1D bitmapindex: The index to check (0-255)
true if the index has been claimed, false otherwise
Implementation:
- Tracking execution status within a single transaction
- Packing multiple claim states into a single storage slot
setClaimed1D
Marks a specific index as claimed in a 1D bitmap.claimedBitMap: Auint256value encoding a 1D bitmapindex: The index to mark as claimed (0-255)
Usage in Across Protocol
Root Bundle Execution Flow
-
Proposal: Data worker proposes a root bundle containing merkle roots for:
- Pool rebalances (HubPool → SpokePools token transfers)
- Relayer refunds (reimbursements for fills)
- Slow fills (protocol-executed fills)
-
Execution Request: After liveness period, actors call execution functions with:
- The leaf data (rebalance, refund, or slow fill)
- A merkle proof showing inclusion in the root
-
Verification: Contract uses MerkleLib to:
- Verify the proof against the stored root
- Check if the leaf has already been executed
- Mark the leaf as claimed to prevent re-execution
Example: Relayer Refund Execution
Bitmap Efficiency
Why Bitmaps?
Bitmaps provide significant gas savings compared to alternative approaches:| Approach | Gas per Claim Check | Gas per Claim Set |
|---|---|---|
mapping(uint256 => bool) | ~2,100 (cold) | ~20,000 (cold) |
| Bitmap (2D) | ~2,100 (cold) | ~20,000 (cold, first bit in word) |
| Bitmap (2D) | ~800 (warm) | ~5,000 (warm, same word) |
1D vs 2D Bitmaps
- 1D (
isClaimed1D,setClaimed1D): Best for single-storage-slot scenarios or temporary tracking within a transaction - 2D (
isClaimed,setClaimed): Best for persistent, unbounded claim tracking across many root bundles
Security Considerations
Double-Execution Prevention
The claim tracking system prevents double-execution attacks:- Without claim tracking, an attacker could execute the same refund leaf multiple times
- Bitmaps ensure each leaf can only be executed once
Merkle Proof Validation
MerkleLib relies on OpenZeppelin’sMerkleProof.verify(), which:
- Implements standard merkle proof verification
- Is battle-tested across many protocols
- Prevents proof forgery and inclusion attacks
Leaf Encoding
Leaves are encoded usingabi.encode() before hashing:
- Consistent encoding across different struct types
- No possibility of collision between different leaf types
- Compatibility with off-chain merkle tree construction
Attribution
The claim tracking functions are adapted from Uniswap’s MerkleDistributor with minor modifications for Across Protocol’s use cases.Related Contracts
- HubPool: Uses MerkleLib to execute pool rebalance leaves
- SpokePool: Uses MerkleLib to execute relayer refund and slow fill leaves
- MerkleProof (OpenZeppelin): Underlying verification logic