BaseTokenOpsMerkleDistributor
The BaseTokenOpsMerkleDistributor
is an abstract base contract that provides the core functionality shared by all TokenOps Merkle Distributor implementations. It handles Merkle proof verification, claim tracking, access control, and grace period management.
Overview
This contract serves as the foundation for:
- ERC20 Token Distributors - For distributing ERC20 tokens
- Native Token Distributors - For distributing blockchain native tokens (ETH, MATIC, etc.)
Key Features
🔒 Security Foundation
- Merkle Proof Verification: Cryptographically secure claim validation
- Access Control: Owner and fee collector role management
- Grace Period Protection: 7-day withdrawal protection period
- Claim Tracking: Efficient bitmap-based claim status tracking
⏰ Time Management
- Configurable Periods: Start and end times for claim windows
- Grace Period: Automatic 7-day grace period after first claim
- Expiration Handling: Proper handling of expired campaigns
🛡️ Anti-Fraud Measures
- Single Claim: Each index can only be claimed once
- Proof Validation: Invalid proofs are rejected
- Time Window Enforcement: Claims only allowed within specified periods
Contract Details
Core Functions
Claim Verification
isClaimed
Checks if a specific index has already been claimed.
function isClaimed(uint256 index) public view virtual returns (bool);
Parameters:
index
: The index in the Merkle tree to check
Returns:
bool
: True if the index has been claimed, false otherwise
Example:
const hasClaimed = await distributor.isClaimed(42);
if (hasClaimed) {
console.log("Index 42 has already been claimed");
}
_verifyAndMarkClaimed
Internal function that verifies a Merkle proof and marks the index as claimed.
function _verifyAndMarkClaimed(
uint256 index,
address account,
uint256 amount,
bytes32[] calldata merkleProof
) internal returns (bool);
Purpose:
- Validates the Merkle proof against the stored root
- Marks the index as claimed to prevent double-claiming
- Returns success status
Time Management
getFirstClaimTime
Returns the timestamp of the first claim made.
function getFirstClaimTime() public view virtual returns (uint40);
Returns:
uint40
: Timestamp of first claim, or 0 if no claims have been made
hasExpired
Checks if the distribution period has ended.
function hasExpired() public view virtual returns (bool);
Returns:
bool
: True if current time is past the end time
gracePeriodStatus
Checks if the grace period has passed.
function gracePeriodStatus() external view returns (bool);
Returns:
bool
: True if grace period has passed, false otherwise
Grace Period Logic:
- Grace period starts after the first claim
- Lasts for 7 days (604,800 seconds)
- Protects against premature admin withdrawals
gracePeriodRemainingTime
Returns remaining time until grace period ends.
function gracePeriodRemainingTime() external view returns (uint256);
Returns:
uint256
: Seconds remaining until grace period ends (0 if ended or not started)
Access Control
transferFeeCollectorRole
Transfers the fee collector role to a new address.
function transferFeeCollectorRole(address newFeeCollector) external onlyFeeCollector;
Parameters:
newFeeCollector
: Address of the new fee collector
Access: Only current fee collector
State Variables
Immutable Variables
These are set at deployment and cannot be changed:
uint256 public immutable DEPLOYMENT_BLOCK_NUMBER; // Block number of deployment
bytes32 public immutable merkleRoot; // Merkle tree root hash
uint32 public immutable startTime; // Claim period start time
uint32 public immutable endTime; // Claim period end time
ITypes.FeeType public immutable feeType; // Fee type (Gas or Token)
uint256 public immutable fee; // Fee amount
Mutable Variables
address public feeCollector; // Current fee collector address
Internal Variables
uint40 internal _firstClaimTime; // Timestamp of first claim
mapping(uint256 => uint256) private claimedBitMap; // Bitmap for claim tracking
Events
FeeCollectorUpdated
Emitted when the fee collector address is changed.
event FeeCollectorUpdated(
address indexed oldFeeCollector,
address indexed newFeeCollector
);
Modifiers
onlyFeeCollector
Restricts function access to the fee collector address.
modifier onlyFeeCollector();
Usage:
function withdrawFees() external onlyFeeCollector {
// Only fee collector can execute this
}
Implementation Details
Claim Bitmap
The contract uses a gas-efficient bitmap to track claimed indices:
// Check if index is claimed
uint256 claimedWordIndex = index / 256;
uint256 claimedBitIndex = index % 256;
uint256 claimedWord = claimedBitMap[claimedWordIndex];
return (claimedWord & (1 << claimedBitIndex)) != 0;
Benefits:
- Gas Efficient: Multiple claims tracked in single storage slot
- Scalable: Supports unlimited number of recipients
- Atomic: Setting claim status is atomic operation
Merkle Proof Verification
The contract validates proofs using OpenZeppelin's MerkleProof library:
bytes32 leaf = keccak256(abi.encodePacked(index, account, amount));
require(MerkleProof.verify(merkleProof, merkleRoot, leaf), "Invalid proof");
Security Features:
- Cryptographic Security: Based on SHA-256 hash function
- Tamper Proof: Any modification invalidates the proof
- Efficient: O(log n) verification time
Grace Period Implementation
function _hasGracePeriodPassed() internal view returns (bool) {
uint40 firstClaimTime = _firstClaimTime;
return firstClaimTime != 0 && block.timestamp >= firstClaimTime + 7 days;
}
Protection Mechanism:
- User Protection: Prevents premature admin withdrawals
- Fair Access: Ensures users have time to claim
- Automatic: No manual intervention required
Usage Patterns
Inheritance Pattern
contract TokenOpsMerkleDistributor is
BaseTokenOpsMerkleDistributor,
ITokenOpsMerkleDistributor
{
constructor(
// ... parameters
) BaseTokenOpsMerkleDistributor(
merkleRoot_,
startTime_,
endTime_,
feeType_,
fee_,
feeCollector_,
admin
) {
// Additional initialization
}
function claim(
uint256 index,
address account,
uint256 amount,
bytes32[] calldata merkleProof
) external payable override {
// Validate timing
require(block.timestamp >= startTime, "Claim not started");
require(block.timestamp <= endTime, "Claim window finished");
// Use base functionality
require(_verifyAndMarkClaimed(index, account, amount, merkleProof), "Invalid claim");
// Record first claim
_recordFirstClaim();
// Implementation-specific logic
_processTokenTransfer(account, amount);
}
}
Security Considerations
Access Control
- Owner Role: Full contract control, can withdraw tokens
- Fee Collector Role: Can only withdraw fees, separate from owner
- Role Transfer: Fee collector role can be transferred
Timing Security
- Start Time Validation: Claims cannot happen before start time
- End Time Validation: Claims cannot happen after end time
- Grace Period: 7-day protection against premature withdrawals
Merkle Tree Security
- Root Immutability: Merkle root cannot be changed after deployment
- Proof Validation: All proofs are cryptographically verified
- Index Uniqueness: Each index can only be claimed once
Gas Optimization
Efficient Storage
// Packed storage for gas efficiency
struct ClaimData {
uint40 firstClaimTime; // Fits in single slot with other data
// ... other efficiently packed data
}
Bitmap Usage
- Batch Operations: Multiple claim statuses in single storage slot
- Minimal Storage: 1 bit per recipient instead of 256 bits
- Gas Savings: Significant reduction in storage costs
Error Handling
Common Errors
Error | Cause | Prevention |
---|---|---|
InvalidMerkleRoot() | Zero or invalid root | Validate root before deployment |
InvalidStartTime() | Start time in past | Ensure future start time |
EndTimeInPast() | End time before current | Validate end time |
InvalidAddress() | Zero address provided | Validate all addresses |
ClaimNotStarted() | Before start time | Check timing before claim |
OnlyFeeCollector() | Wrong caller | Use fee collector address |
Best Practices
- Validation: Always validate parameters before deployment
- Timing: Ensure proper time windows
- Testing: Test all edge cases thoroughly
- Access Control: Properly manage roles
Related Contracts
- TokenOpsMerkleDistributor - ERC20 implementation
- TokenOpsMerkleDistributorNative - Native token implementation
- ITokenOpsMerkleDistributorBase - Base interface
Testing Utilities
Mock Implementation
contract MockDistributor is BaseTokenOpsMerkleDistributor {
constructor(
bytes32 merkleRoot_,
uint32 startTime_,
uint32 endTime_,
uint256 fee_,
address feeCollector_
) BaseTokenOpsMerkleDistributor(
merkleRoot_,
startTime_,
endTime_,
ITypes.FeeType.Gas,
fee_,
feeCollector_,
msg.sender
) {}
function testVerifyAndMarkClaimed(
uint256 index,
address account,
uint256 amount,
bytes32[] calldata proof
) external returns (bool) {
return _verifyAndMarkClaimed(index, account, amount, proof);
}
}
This base contract provides the secure foundation for all TokenOps distributors. Its modular design ensures consistency while allowing for specialized implementations.