The MerkleBatchPayout contract enables gasless, scalable batch payments using Merkle proofs. Recipients can claim their payments by providing a valid Merkle proof, eliminating the need for the payer to execute individual transactions.
- Node.js and Yarn installed
- Hardhat configured
- Private key with sufficient funds for deployment
- Network RPC URL configured in
hardhat.config.ts
# Deploy to local hardhat network (for testing)
npx hardhat run scripts/deployMerkleBatchPayout.ts --network hardhat
# Deploy to Base Sepolia (testnet)
npx hardhat run scripts/deployMerkleBatchPayout.ts --network basetest
# Deploy to Base Mainnet
npx hardhat run scripts/deployMerkleBatchPayout.ts --network base
# Deploy to BSC Mainnet
npx hardhat run scripts/deployMerkleBatchPayout.ts --network bsc
# Deploy to Ethereum Mainnet
npx hardhat run scripts/deployMerkleBatchPayout.ts --network mainnet
# Deploy to Polygon Mainnet
npx hardhat run scripts/deployMerkleBatchPayout.ts --network polygonAfter deployment, save the contract address from the output:
MERKLE_BATCH_PAYOUT_ADDRESS=0x...
Use the verification command provided in the deployment output:
npx hardhat verify --network <network-name> <CONTRACT_ADDRESS> "<ROUTER_ADDRESS>" "<WETH_ADDRESS>" "<DEPLOYER_ADDRESS>" "<DEPLOYER_ADDRESS>"Example for Base Sepolia:
npx hardhat verify --network basetest 0x5FbDB2315678afecb367f032d93F642f64180aa3 "0x1689E7B1F10000AE47eBfE339a4f69dECd19F602" "0x4200000000000000000000000000000000000006" "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"After deploying MerkleBatchPayout, you need to register it with the UniversalDexRouter to enable swap-to-fund functionality.
If you haven't deployed UniversalDexRouter yet, you can register MerkleBatchPayout automatically during deployment:
# Set the MerkleBatchPayout address as an environment variable
export MERKLE_BATCH_PAYOUT_ADDRESS=0x...
# Deploy UniversalDexRouter (will automatically register MerkleBatchPayout)
npx hardhat run scripts/deployUniversalDexRouter.ts --network <network-name>The UniversalDexRouter constructor now accepts an optional 6th parameter for the MerkleBatchPayout contract address. If provided (not address(0)), it will automatically register the contract during deployment.
If UniversalDexRouter is already deployed, you can add MerkleBatchPayout using the provided script:
export UNIVERSAL_DEX_ROUTER_ADDRESS=0x...
export MERKLE_BATCH_PAYOUT_ADDRESS=0x...
npx hardhat run scripts/addMerkleBatchPayoutSupport.ts --network <network-name>Or manually using Hardhat console:
npx hardhat console --network <network-name>const router = await ethers.getContractAt("UniversalDexRouter", "0x...");
await router.setSupportedMerkleBatchPayoutContract("0x...", true);See scripts/createAndClaimBatch.ts for a complete example. Here's a quick overview:
import { generateBatchData, PaymentEntry } from "./scripts/merkleHelper";
// 1. Define payments
const payments: PaymentEntry[] = [
{
receiverAddress: "0x...",
amount: ethers.parseEther("100").toString(),
claimableDate: Math.floor(Date.now() / 1000) + 86400, // 1 day from now
memo: "Payment 1"
},
{
receiverAddress: "0x...",
amount: ethers.parseEther("50").toString(),
claimableDate: Math.floor(Date.now() / 1000) + 86400,
memo: "Payment 2"
}
];
// 2. Generate batch data
const signer = await ethers.getSigner();
const timestamp = Math.floor(Date.now() / 1000);
const chainId = (await ethers.provider.getNetwork()).chainId;
const batchData = await generateBatchData(
payments,
"unique-salt-123", // Unique salt for this batch
signer,
FUNDING_TOKEN_ADDRESS,
timestamp,
Number(chainId)
);
// 3. Create batch via UniversalDexRouter (with swap funding)
const router = await ethers.getContractAt("UniversalDexRouter", ROUTER_ADDRESS);
const totalAmount = ethers.parseEther("150");
await router.createMerkleBatch(
MERKLE_BATCH_PAYOUT_ADDRESS,
[{
amountIn: ethers.parseEther("160"),
amountOut: totalAmount,
path: [TOKEN_IN_ADDRESS, FUNDING_TOKEN_ADDRESS]
}],
batchData.batchHash,
timestamp,
batchData.signerAddress,
batchData.merkleRoot,
Math.floor(Date.now() / 1000) + 86400 * 30, // 30 days for creator withdrawal
FUNDING_TOKEN_ADDRESS,
batchData.signature,
totalAmount
);import { getPaymentProof } from "./scripts/merkleHelper";
// Get proof for specific payment
const proof = getPaymentProof(batchData.tree, batchData.batchId, payments[0]);
// Claim via UniversalDexRouter
await router.connect(receiver).claimMerkleBatchPayment(
MERKLE_BATCH_PAYOUT_ADDRESS,
batchData.batchId,
payments[0].receiverAddress,
payments[0].amount,
payments[0].claimableDate,
payments[0].memo,
proof
);The deployment script includes pre-configured settings for:
- Base Sepolia (Chain ID: 84532) - Testnet
- Base Mainnet (Chain ID: 8453)
- BSC Mainnet (Chain ID: 56)
- Ethereum Mainnet (Chain ID: 1)
- Polygon Mainnet (Chain ID: 137)
-
Role Management: The deployer receives both DEFAULT_ADMIN_ROLE and ADMIN_ROLE. Consider transferring these to a multisig wallet for production.
-
Batch Creator Withdrawal: Batch creators can withdraw unclaimed funds after
creatorWithdrawDate. Set this appropriately based on your use case. -
Merkle Proof Security: The batchId includes the chainId to prevent cross-chain replay attacks.
-
Signature Verification: All batches require valid signatures from the batch creator to prevent unauthorized batch creation.
- Insufficient Balance: Ensure the deployer account has enough native currency for gas fees
- Invalid Router/WETH: Verify the router and WETH addresses for your network
- Network Configuration: Check
hardhat.config.tshas the correct network configuration
- Not Admin: Ensure you're using the admin account (deployer by default)
- Wrong Network: Verify you're on the same network where both contracts are deployed
- Contract Not Deployed: Confirm both contracts are deployed and addresses are correct