Cutting Gas Fees: Building a Native and ERC20 Batch Transfer Contract

Netanel Basal
Netanel Basal
Published in
3 min readSep 3, 2023

In this article, We’ll create a Batch Transfer Smart Contract that allows users to send either native or ERC-20 tokens to multiple recipients in a single transaction.

Batch transfers dramatically reduce the amount of gas required for transactions, as you can send assets to multiple recipients with a single transaction fee. In contrast, sending assets individually to each address would result in substantially higher gas costs. This efficiency can be crucial when the network congestion leads to elevated gas prices. Let’s get started.

First, The contract imports the OpenZeppelin library, specifically the IERC20 interface. This interface is crucial for interacting with ERC-20 tokens:

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

Next, we’ll create the contract and introduce two key events: ERC20Transfer and NativeTransfer. These events keep a clear record of token transactions as they occur:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract BatchTransfer {
event ERC20Transfer(address indexed token,
address indexed from,
address indexed to,
uint256 amount);
event NativeTransfer(address indexed from,
address indexed to,
uint256 amount);
}

Next, we’ll create the main function in this contract, called batchTransfer, which lets us send tokens to multiple recipients:

pragma solidity ^0.8.9;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract BatchTransfer {
event ERC20Transfer(address indexed token,
address indexed from,
address indexed to,
uint256 amount);
event NativeTransfer(address indexed from,
address indexed to,
uint256 amount);

function batchTransfer(
address[] calldata recipients,
uint256[] calldata amounts,
address tokenAddress
) external payable {
if(recipients.length != amounts.length) {
revert ArraysLengthMismatch(recipients.length, amounts.length);
}

if (tokenAddress == address(0)) {
for (uint256 i = 0; i < recipients.length; i++) {
address recipient = recipients[i];
payable(recipient).transfer(amounts[i]);
emit NativeTransfer(msg.sender, recipient, amounts[i]);
}
} else {
IERC20 token = IERC20(tokenAddress);

for (uint256 i = 0; i < recipients.length; i++) {
address recipient = recipients[i];
token.transferFrom(msg.sender, recipient, amounts[i]);
emit ERC20Transfer(tokenAddress, msg.sender, recipient, amounts[i]);
}
}

if(address(this).balance > 0) {
payable(msg.sender).transfer(address(this).balance);
}
}
}

First, we validate that the lengths of the recipients and amounts arrays match. If they don’t match, the transaction is reverted with an error, ensuring data integrity. In this example, we focus on this aspect for brevity, but it’s strongly advised to include as many validations as possible.

Next, it transfers assets to the specified recipients based on whether tokenAddress represents the zero address (indicating a native token) or the address of an ERC-20 token.

When tokenAddress is the zero address, it conducts the transfer of native token using the transfer function. Conversely, if tokenAddress doesn’t correspond to the zero address, it employs the transferFrom function from the ERC-20 token contract to execute the transfers. Additionally, it's crucial to note that in the latter case, an initial approve or permit must be sent to authorize the deployed contract address for token transfers.

After the transfers are completed, any remaining balance in the contract is sent back to the sender, ensuring that the contract’s balance is appropriately managed.

You can play with the code using the Remix online IDE:

--

--

Netanel Basal
Netanel Basal

Written by Netanel Basal

A FrontEnd Tech Lead, blogger, and open source maintainer. The founder of ngneat, husband and father.

Responses (1)