TokenOpsMerkleDistributorNative
The TokenOpsMerkleDistributorNative
contract specializes in distributing native blockchain tokens (ETH, MATIC, BNB, etc.) using Merkle trees. It provides a streamlined, gas-efficient solution for native token airdrops without the complexity of ERC20 token handling.
Overview
This contract enables:
- Native Token Airdrops: Distribute ETH, MATIC, BNB, or other native tokens
- Gas-Only Fees: Simplified fee structure using native tokens
- Efficient Claims: Optimized for native token transfers
- Simplified Admin: Streamlined management for native token campaigns
Key Features
🎯 Native Token Focus
- Direct Native Transfers: No ERC20 token interactions
- Gas-Only Fees: Uses native tokens for fees
- Efficient Storage: Optimized for native token handling
- Simplified Logic: Reduced complexity compared to ERC20 version
💰 Fee Structure
- Gas Fees Only: Always uses native tokens for fees
- Automatic Calculation: Fee reserves calculated at deployment
- Transparent Tracking: Clear accounting of fees vs. distribution amounts
🔒 Security Features
- Merkle Proof Validation: Cryptographically secure claims
- Single Claim Protection: Each index can only be claimed once
- Grace Period Protection: 7-day withdrawal protection
- Balance Validation: Proper native token balance checks
Contract Details
Core Functions
Claiming Function
claim
Claim native tokens using Merkle proof validation.
function claim(
uint256 index,
address account,
uint256 amount,
bytes32[] calldata merkleProof
) external payable;
Parameters:
index
: Position in the Merkle treeaccount
: Address that should receive the native tokensamount
: Amount of native tokens to claim (in wei)merkleProof
: Merkle proof validating the claim
Requirements:
- Must be within claim window (startTime to endTime)
- Must provide valid Merkle proof
- Index must not be already claimed
- Must pay required gas fee
Example:
const tx = await nativeDistributor.claim(
42, // index
userAddress, // account
ethers.parseEther("0.5"), // amount (0.5 ETH)
merkleProof, // proof array
{ value: gasFee } // gas fee payment
);
Admin Functions
withdraw
Allows distributor admin to withdraw remaining native tokens.
function withdraw() external onlyOwner;
Access: Only contract owner (distributor admin)
Requirements:
- Grace period must have passed (7 days after first claim)
- Cannot withdraw during active claim period (unless expired)
Example:
// Check if withdrawal is allowed
const hasExpired = await nativeDistributor.hasExpired();
const gracePeriodPassed = await nativeDistributor.gracePeriodStatus();
if (hasExpired || gracePeriodPassed) {
const tx = await nativeDistributor.withdraw();
await tx.wait();
console.log("Remaining native tokens withdrawn");
}
Fee Collector Functions
withdrawGasFee
Withdraw collected gas fees (paid in native tokens).
function withdrawGasFee(address recipient, uint256 amount) external onlyFeeCollector;
Parameters:
recipient
: Address to receive the feesamount
: Amount to withdraw (0 = withdraw all)
Access: Only fee collector
Example:
// Withdraw all collected gas fees
const tx = await nativeDistributor.withdrawGasFee(feeCollectorAddress, 0);
await tx.wait();
State Variables
Immutable Configuration
ITypes.FeeType public constant FEE_TYPE = ITypes.FeeType.Gas; // Always gas fees
uint256 public totalAmount; // Total distribution amount
Dynamic State
uint256 public numNativeReservedForFee; // Native tokens reserved for fees
Events
Claimed
Emitted when native tokens are claimed.
event Claimed(
uint256 indexed index,
address indexed account,
uint256 amount
);
Withdrawn
Emitted when admin withdraws remaining tokens.
event Withdrawn(
address indexed admin,
uint256 amount
);
GasFeeWithdrawn
Emitted when fee collector withdraws gas fees.
event GasFeeWithdrawn(
address indexed recipient,
uint256 amount
);
Implementation Details
Native Token Handling
The contract is specifically designed for native token distribution:
// Native token transfer (no ERC20 interactions)
(bool success, ) = account.call{value: amount}("");
require(success, "Transfer failed");
Fee Calculation
Since only gas fees are supported, the calculation is straightforward:
// At deployment, calculate total fee reserves
uint256 expectedClaims = // estimate based on Merkle tree size
numNativeReservedForFee = expectedClaims * fee;
Balance Management
The contract tracks native token balances carefully:
// Total contract balance
uint256 totalBalance = address(this).balance;
// Available for distribution
uint256 availableForDistribution = totalBalance - numNativeReservedForFee;
// Ensure sufficient balance for claims
require(availableForDistribution >= amount, "Insufficient balance");
Deployment Pattern
Factory Integration
The contract is typically deployed through the factory:
// Deploy through factory
address nativeDistributor = factory.newMerkleDistributorNative{value: totalAmount}(
merkleRoot,
startTime,
endTime
);
Funding Requirements
The contract must be funded at deployment:
// Calculate required funding
const totalDistributionAmount = ethers.parseEther("100"); // 100 ETH to distribute
const estimatedClaims = 1000; // Expected number of claims
const gasFee = ethers.parseEther("0.001"); // 0.001 ETH per claim
const totalFeeReserve = gasFee * BigInt(estimatedClaims);
const totalRequired = totalDistributionAmount + totalFeeReserve;
// Deploy with funding
const tx = await factory.newMerkleDistributorNative(
merkleRoot,
startTime,
endTime,
{ value: totalRequired }
);
Usage Examples
Basic Native Token Claim
import { ethers } from "ethers";
// Connect to native distributor
const nativeDistributor = new ethers.Contract(
distributorAddress,
nativeDistributorABI,
signer
);
// Prepare claim data
const claimData = {
index: 42,
account: userAddress,
amount: ethers.parseEther("0.5"), // 0.5 ETH
proof: merkleProof
};
// Check claim eligibility
const hasClaimed = await nativeDistributor.isClaimed(claimData.index);
if (hasClaimed) {
throw new Error("Already claimed");
}
// Get fee amount
const gasFee = await nativeDistributor.fee();
// Claim native tokens
const tx = await nativeDistributor.claim(
claimData.index,
claimData.account,
claimData.amount,
claimData.proof,
{ value: gasFee }
);
await tx.wait();
console.log("Native tokens claimed successfully");
Admin Withdrawal
// Check withdrawal conditions
const [hasExpired, gracePeriodPassed, contractBalance] = await Promise.all([
nativeDistributor.hasExpired(),
nativeDistributor.gracePeriodStatus(),
ethers.provider.getBalance(distributorAddress)
]);
console.log("Campaign expired:", hasExpired);
console.log("Grace period passed:", gracePeriodPassed);
console.log("Contract balance:", ethers.formatEther(contractBalance), "ETH");
// Withdraw if conditions are met
if (hasExpired || gracePeriodPassed) {
const tx = await nativeDistributor.withdraw();
await tx.wait();
console.log("Remaining tokens withdrawn");
} else {
console.log("Cannot withdraw yet");
}
Fee Collection
// Check fee balance
const feeBalance = await nativeDistributor.numNativeReservedForFee();
console.log("Fee balance:", ethers.formatEther(feeBalance), "ETH");
// Withdraw fees (only fee collector can do this)
const feeCollectorSigner = // ... get fee collector signer
const tx = await nativeDistributor.connect(feeCollectorSigner)
.withdrawGasFee(feeCollectorAddress, 0); // 0 = withdraw all
await tx.wait();
console.log("Gas fees withdrawn");
Security Considerations
Native Token Security
- Direct Transfers: Uses low-level calls for native token transfers
- Return Value Checking: Always checks transfer success
- Balance Validation: Ensures sufficient balance before transfers
- Fee Separation: Clear separation between fees and distribution amounts
Access Control
- Owner Role: Can withdraw remaining tokens after grace period
- Fee Collector Role: Can withdraw collected fees
- Role Separation: Roles are completely separate
Timing Protection
- Grace Period: 7-day protection against airdrop mistakes
- Expiration Handling: Proper handling of campaign expiration
- Claim Window: Strict enforcement of claim timing
Gas Optimization
Native Token Efficiency
// Direct native token transfer (more efficient than ERC20)
(bool success, ) = account.call{value: amount}("");
require(success, "Transfer failed");
Storage Optimization
// Efficient storage packing
uint256 public totalAmount; // Total distribution amount
uint256 public numNativeReservedForFee; // Fee reserves
Error Handling
Common Errors
Error | Cause | Solution |
---|---|---|
InvalidProof() | Invalid Merkle proof | Regenerate proof from valid tree |
ClaimWindowFinished() | Past end time | Campaign has ended |
InsufficientFee() | Insufficient gas fee | Send correct fee amount |
NotEnoughNativeTokens() | Insufficient balance | Ensure contract is properly funded |
TransferFailed() | Native transfer failed | Check recipient address |
Best Practices
- Proper Funding: Ensure contract is funded with enough native tokens
- Fee Calculation: Account for gas fees in total funding
- Balance Monitoring: Monitor contract balance regularly
- Grace Period: Respect withdrawal timing restrictions
Comparison with ERC20 Version
Feature | Native Distributor | ERC20 Distributor |
---|---|---|
Token Type | Native (ETH, MATIC, etc.) | ERC20 tokens |
Fee Types | Gas fees only | Gas fees or token fees |
Staking | Not supported | Full staking integration |
Bonuses | Not supported | Bonus rewards available |
Complexity | Lower | Higher |
Gas Efficiency | More efficient | Less efficient |
Use Cases | Native token airdrops | Token project airdrops |
Deployment Checklist
Pre-Deployment
- Merkle Tree Generated: Valid Merkle root calculated
- Timing Validated: Start and end times are reasonable
- Funding Calculated: Total amount + fee reserves
- Addresses Verified: Fee collector and admin addresses correct
Deployment
- Factory Deployment: Deploy through factory with proper funding
- Event Monitoring: Set up event listeners
- Address Recording: Record distributor address
- Verification: Verify contract on block explorer
Post-Deployment
- Balance Verification: Confirm contract has correct balance
- Configuration Check: Verify all parameters are correct
- Access Control: Test admin and fee collector functions
- Claim Testing: Test sample claims
Related Contracts
- BaseTokenOpsMerkleDistributor - Base contract
- TokenOpsMerkleDistributor - ERC20 version
- TokenOpsMerkleDistributorFactory - Factory contract
- ITokenOpsMerkleDistributorNative - Interface
This contract provides a streamlined solution for native token distribution, optimized for efficiency and simplicity.