Skip to main content

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 tree
  • account: Address that should receive the native tokens
  • amount: 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 fees
  • amount: 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

ErrorCauseSolution
InvalidProof()Invalid Merkle proofRegenerate proof from valid tree
ClaimWindowFinished()Past end timeCampaign has ended
InsufficientFee()Insufficient gas feeSend correct fee amount
NotEnoughNativeTokens()Insufficient balanceEnsure contract is properly funded
TransferFailed()Native transfer failedCheck recipient address

Best Practices

  1. Proper Funding: Ensure contract is funded with enough native tokens
  2. Fee Calculation: Account for gas fees in total funding
  3. Balance Monitoring: Monitor contract balance regularly
  4. Grace Period: Respect withdrawal timing restrictions

Comparison with ERC20 Version

FeatureNative DistributorERC20 Distributor
Token TypeNative (ETH, MATIC, etc.)ERC20 tokens
Fee TypesGas fees onlyGas fees or token fees
StakingNot supportedFull staking integration
BonusesNot supportedBonus rewards available
ComplexityLowerHigher
Gas EfficiencyMore efficientLess efficient
Use CasesNative token airdropsToken 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

This contract provides a streamlined solution for native token distribution, optimized for efficiency and simplicity.