A gas-optimised smart contract for batch ERC-20 transfers of USDT and USDC, deployed on Polygon, BSC, and Ethereum mainnet. Includes a fully-typed TypeScript SDK and helper scripts for every step of the workflow.
| Chain | Address | Explorer |
|---|---|---|
| Polygon | 0x4cDAe2B209C219F5705A21DDCeA8d1568dfA978f |
polygonscan.com |
| BSC | 0x4cDAe2B209C219F5705A21DDCeA8d1568dfA978f |
bscscan.com |
| Ethereum | 0x4cDAe2B209C219F5705A21DDCeA8d1568dfA978f |
etherscan.io |
| Chain | Address | Explorer |
|---|---|---|
| Polygon | 0x7FAc60305d4D5e814cae28DeF7D910C99Dd86790 |
polygonscan.com |
| BSC | 0x86bfBde536cdD1A9ED08A2FaC786086Ab44797DB |
bscscan.com |
| Ethereum | 0x86bfBde536cdD1A9ED08A2FaC786086Ab44797DB |
etherscan.io |
- Batch up to 100+ transfers in a single transaction (~68% cheaper than individual transfers)
- Supports USDT and USDC on all three chains
- Pre-flight
validateBatch()— catches errors before spending gas - Two-step ownership transfer (safe handover pattern)
- Reentrancy guard
- BatchSend — batch-send native tokens (POL/BNB/ETH) in one call
- Fully typed TypeScript SDK with ethers v6 for both contracts
- Helper scripts for deploy, approve, transfer, and transaction decoding
| Token | Address | Decimals |
|---|---|---|
| USDT | 0xc2132D05D31c914a87C6611C10748AEb04B58e8F |
6 |
| USDC | 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 |
6 |
| Token | Address | Decimals |
|---|---|---|
| USDT | 0x55d398326f99059fF775485246999027B3197955 |
18 |
| USDC | 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d |
18 |
| Token | Address | Decimals |
|---|---|---|
| USDT | 0xdAC17F958D2ee523a2206206994597C13D831ec7 |
6 |
| USDC | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 |
6 |
⚠ BSC USDT uses 18 decimals — use
DECIMALS_18from the SDK when building amounts.
npm installcp .example.env .envEdit .env and fill in:
PRIVATE_KEY=0xYOUR_OWNER_PRIVATE_KEY
W1_PRIVATE_KEY=0xYOUR_SENDER_WALLET_PRIVATE_KEY
RPC_URL=https://rpc.ankr.com/polygon/YOUR_KEY
BSC_RPC_URL=https://bsc-dataseed1.binance.org/
ETH_RPC_URL=https://rpc.ankr.com/eth/YOUR_KEYnpx hardhat compileDeploy both contracts (MultiStableTransfer + BatchSend) to all chains in one run:
node scripts/deploy.jsDeploy a specific contract or chain:
node scripts/deploy.js multi # MultiStableTransfer only, all chains
node scripts/deploy.js batch # BatchSend only, all chains
node scripts/deploy.js multi polygon # MultiStableTransfer on Polygon only
node scripts/deploy.js batch bsc # BatchSend on BSC onlyThe sender wallet (W1_PRIVATE_KEY) must approve the contract to spend its tokens before any transfer. Run once per chain:
node scripts/approve.jsEdit the TRANSFERS array in scripts/multitransfer.js, then:
node scripts/multitransfer.jsnode scripts/check.jsnode scripts/fetchtxn.js 0xYOUR_TX_HASHFetches live gas prices from each chain's RPC and live token prices from Binance:
node scripts/estimate-cost.jsnode scripts/estimate-batchsend.jsnode scripts/generate-transfer-report.jsWrites TRANSFER_CAPACITY.md with live cost tables showing how many transfers you can execute per dollar on each chain.
npm run build # compiles sdk/src/ → sdk/dist/
npm run build:watch # watch modenpx tsx sdk/example.tsimport { ethers } from "ethers";
import {
MultiStableTransferSDK,
POLYGON,
CONTRACT_ADDRESS,
type TransferData,
} from "./sdk/src/index.js";
const provider = new ethers.JsonRpcProvider(RPC_URL);
const owner = new ethers.Wallet(OWNER_PRIVATE_KEY, provider);
const sdk = new MultiStableTransferSDK({
contractAddress: CONTRACT_ADDRESS,
signerOrProvider: owner,
});
// Build transfers
const transfers: TransferData[] = [
{
token: POLYGON.USDT,
from: "0xSENDER_ADDRESS",
to: "0xRECIPIENT_ADDRESS",
amount: MultiStableTransferSDK.parseAmount("100"), // 100 USDT
},
];
// Validate before sending (no gas)
const { valid, reasons } = await sdk.validateBatch(transfers);
// Execute
const receipt = await sdk.multiTransfer(transfers);
// Decode events
const { transfers: decoded, totalTransfers } =
MultiStableTransferSDK.decodeReceipt(receipt);Pass a Signer for write operations or a Provider for read-only.
| Method | Returns | Description |
|---|---|---|
owner() |
Promise<string> |
Current contract owner |
pendingOwner() |
Promise<string> |
Pending owner (two-step transfer) |
usdt() |
Promise<string> |
USDT address registered in contract |
usdc() |
Promise<string> |
USDC address registered in contract |
validateBatch(transfers) |
Promise<ValidateBatchResult> |
Pre-flight check, no gas |
getAllowances(wallets, tokens) |
Promise<bigint[]> |
Batch allowance query |
allowance(wallet, token) |
Promise<bigint> |
Single allowance check |
balanceOf(wallet, token) |
Promise<bigint> |
ERC-20 balance |
| Method | Returns | Description |
|---|---|---|
multiTransfer(transfers, opts?) |
Promise<TransactionReceipt> |
Execute batch (owner only) |
approve(token, signer, amount?) |
Promise<TransactionReceipt> |
Approve contract to spend tokens |
transferOwnership(newOwner) |
Promise<TransactionReceipt> |
Initiate ownership transfer |
acceptOwnership() |
Promise<TransactionReceipt> |
Complete ownership transfer |
| Method | Description |
|---|---|
parseAmount(amount, decimals?) |
"1.5" → 1_500_000n (default 6 decimals) |
formatAmount(amount, decimals?) |
1_500_000n → "1.5" |
decodeReceipt(receipt) |
Decode TransferExecuted / BatchCompleted events |
import {
POLYGON,
BSC,
ETHEREUM,
DECIMALS_6,
DECIMALS_18,
} from "./sdk/src/index.js";
// Each chain config has: { chainId, USDT, USDC }
console.log(POLYGON.chainId); // 137
console.log(BSC.USDT); // 0x55d398...import { ethers } from "ethers";
import { BatchSendSDK, BATCH_SEND_ADDRESSES } from "./sdk/src/index.js";
const provider = new ethers.JsonRpcProvider(RPC_URL);
const signer = new ethers.Wallet(PRIVATE_KEY, provider);
const sdk = new BatchSendSDK({
contractAddress: BATCH_SEND_ADDRESSES.polygon, // or .bsc / .ethereum
signerOrProvider: signer,
});
// Compute total automatically
const recipients = ["0xAlice", "0xBob", "0xCarol"];
const amounts = [
ethers.parseEther("0.5"),
ethers.parseEther("1.0"),
ethers.parseEther("0.25"),
];
const receipt = await sdk.sendToMany(recipients, amounts);
// Decode the BatchSent event
const result = BatchSendSDK.decodeBatchSentEvent(receipt);
console.log(`Sent to ${result?.totalRecipients} recipients`);
console.log(`Total: ${ethers.formatEther(result?.totalAmount ?? 0n)} ETH`);| Method | Returns | Description |
|---|---|---|
sendToMany(recipients, amounts, opts?) |
Promise<TransactionReceipt> |
Send native tokens to many wallets in one tx |
| Method | Description |
|---|---|
sumAmounts(amounts) |
Sum a bigint array → use as msg.value |
decodeBatchSentEvent(receipt) |
Decode BatchSent event → { sender, totalRecipients, totalAmount } |
import { BATCH_SEND_ADDRESSES } from "./sdk/src/index.js";
BATCH_SEND_ADDRESSES.polygon; // 0x7FAc60305d4D5e814cae28DeF7D910C99Dd86790
BATCH_SEND_ADDRESSES.bsc; // 0x86bfBde536cdD1A9ED08A2FaC786086Ab44797DB
BATCH_SEND_ADDRESSES.ethereum; // 0x86bfBde536cdD1A9ED08A2FaC786086Ab44797DBBatch size has a significant impact on per-transfer cost. Based on real on-chain data (Polygon mainnet, block 86611782):
| Transfers/Call | Gas Units | Savings vs. 1-at-a-time |
|---|---|---|
| 1 | 72,251 | — |
| 10 | 272,510 | 62% |
| 50 | 1,162,550 | 68% |
| 100 | 2,275,100 | 68% |
Run node scripts/generate-transfer-report.js for a live cost breakdown on all chains using real-time Binance prices.
// Execute batch transfers (onlyOwner)
function multiTransfer(TransferData[] calldata transfers) external;
// Pre-flight validation (no gas)
function validateBatch(TransferData[] calldata transfers)
external view returns (bool[] memory valid, string[] memory reasons);
// Batch allowance query
function getAllowances(address[] calldata wallets, address[] calldata tokens)
external view returns (uint256[] memory allowances);
// Two-step ownership
function transferOwnership(address newOwner) external;
function acceptOwnership() external;
struct TransferData {
address token; // must be USDT or USDC
address from; // must have approved this contract
address to;
uint256 amount;
}// Send native tokens to many recipients (payable — msg.value must equal sum of amounts)
function sendToMany(
address[] calldata recipients,
uint256[] calldata amounts
) external payable;
event BatchSent(address indexed sender, uint256 totalRecipients, uint256 totalAmount);- Solidity 0.8.20 — optimizer 200 runs, EVM target: shanghai
- Hardhat v3.4.5
- ethers v6.16.0
- TypeScript (strict, NodeNext ESM)
- dotenv v17
ISC