Skip to main content

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

ErrorCausePrevention
InvalidMerkleRoot()Zero or invalid rootValidate root before deployment
InvalidStartTime()Start time in pastEnsure future start time
EndTimeInPast()End time before currentValidate end time
InvalidAddress()Zero address providedValidate all addresses
ClaimNotStarted()Before start timeCheck timing before claim
OnlyFeeCollector()Wrong callerUse fee collector address

Best Practices

  1. Validation: Always validate parameters before deployment
  2. Timing: Ensure proper time windows
  3. Testing: Test all edge cases thoroughly
  4. Access Control: Properly manage roles

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.