Skip to content

About

Gas-optimised batch transfers of USDT, USDC and native coins, deployed on Ethereum, Polygon and BNB Chain, with a TypeScript SDK.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

MultiStableTransfer

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.


Deployed Contracts

MultiStableTransfer (ERC-20 batch)

Chain Address Explorer
Polygon 0x4cDAe2B209C219F5705A21DDCeA8d1568dfA978f polygonscan.com
BSC 0x4cDAe2B209C219F5705A21DDCeA8d1568dfA978f bscscan.com
Ethereum 0x4cDAe2B209C219F5705A21DDCeA8d1568dfA978f etherscan.io

BatchSend (native token batch)

Chain Address Explorer
Polygon 0x7FAc60305d4D5e814cae28DeF7D910C99Dd86790 polygonscan.com
BSC 0x86bfBde536cdD1A9ED08A2FaC786086Ab44797DB bscscan.com
Ethereum 0x86bfBde536cdD1A9ED08A2FaC786086Ab44797DB etherscan.io

Features

  • 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 Addresses

Polygon

Token Address Decimals
USDT 0xc2132D05D31c914a87C6611C10748AEb04B58e8F 6
USDC 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 6

BSC

Token Address Decimals
USDT 0x55d398326f99059fF775485246999027B3197955 18
USDC 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d 18

Ethereum

Token Address Decimals
USDT 0xdAC17F958D2ee523a2206206994597C13D831ec7 6
USDC 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 6

⚠ BSC USDT uses 18 decimals — use DECIMALS_18 from the SDK when building amounts.


Setup

1. Install dependencies

npm install

2. Configure environment

cp .example.env .env

Edit .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_KEY

3. Compile the contract

npx hardhat compile

Scripts

Deploy

Deploy both contracts (MultiStableTransfer + BatchSend) to all chains in one run:

node scripts/deploy.js

Deploy 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 only

Approve tokens

The sender wallet (W1_PRIVATE_KEY) must approve the contract to spend its tokens before any transfer. Run once per chain:

node scripts/approve.js

Execute a batch transfer

Edit the TRANSFERS array in scripts/multitransfer.js, then:

node scripts/multitransfer.js

Verify deployment

node scripts/check.js

Fetch & decode a transaction

node scripts/fetchtxn.js 0xYOUR_TX_HASH

Estimate deployment cost (live prices)

Fetches live gas prices from each chain's RPC and live token prices from Binance:

node scripts/estimate-cost.js

Estimate BatchSend cost

node scripts/estimate-batchsend.js

Generate transfer capacity report

node scripts/generate-transfer-report.js

Writes TRANSFER_CAPACITY.md with live cost tables showing how many transfers you can execute per dollar on each chain.


TypeScript SDK

Build

npm run build          # compiles sdk/src/ → sdk/dist/
npm run build:watch    # watch mode

Run the example

npx tsx sdk/example.ts

Usage

import { 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);

SDK Reference

new MultiStableTransferSDK({ contractAddress, signerOrProvider })

Pass a Signer for write operations or a Provider for read-only.

Read methods

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

Write methods

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

Static methods

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

Chain constants

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...

BatchSend SDK

Usage

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`);

BatchSendSDK Reference

new BatchSendSDK({ contractAddress, signerOrProvider })

Method Returns Description
sendToMany(recipients, amounts, opts?) Promise<TransactionReceipt> Send native tokens to many wallets in one tx

Static methods

Method Description
sumAmounts(amounts) Sum a bigint array → use as msg.value
decodeBatchSentEvent(receipt) Decode BatchSent event → { sender, totalRecipients, totalAmount }

Deployed addresses constant

import { BATCH_SEND_ADDRESSES } from "./sdk/src/index.js";

BATCH_SEND_ADDRESSES.polygon; // 0x7FAc60305d4D5e814cae28DeF7D910C99Dd86790
BATCH_SEND_ADDRESSES.bsc; // 0x86bfBde536cdD1A9ED08A2FaC786086Ab44797DB
BATCH_SEND_ADDRESSES.ethereum; // 0x86bfBde536cdD1A9ED08A2FaC786086Ab44797DB

Gas & Cost

Batch 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.


Contract Interface

// 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;
}

BatchSend

// 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);

Stack

  • Solidity 0.8.20 — optimizer 200 runs, EVM target: shanghai
  • Hardhat v3.4.5
  • ethers v6.16.0
  • TypeScript (strict, NodeNext ESM)
  • dotenv v17

License

ISC

About

Gas-optimised batch transfers of USDT, USDC and native coins, deployed on Ethereum, Polygon and BNB Chain, with a TypeScript SDK.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages