Skip to content

Instantly share code, notes, and snippets.

@ilhamsa1
Created April 21, 2025 06:43
Show Gist options
  • Save ilhamsa1/4bf2eed7df1e68080fd626d2419884df to your computer and use it in GitHub Desktop.
Save ilhamsa1/4bf2eed7df1e68080fd626d2419884df to your computer and use it in GitHub Desktop.
ERC20BatchTransfer contract allows batch transfers of ERC-20 tokens with support for gasless transactions using permit, reentrancy protection, and event logging. Includes methods for batch transfer, signed permit, and a simple ping counter with a limit.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/metatx/ERC2771Context.sol";
contract ERC20BatchTransfer is ReentrancyGuard, ERC2771Context, Ownable {
using SafeERC20 for IERC20;
constructor(address trustedForwarder) ERC2771Context(trustedForwarder) Ownable(msg.sender) {}
event BatchTransfer(address indexed sender, address indexed token, uint256 totalTransfers);
event TransferDetails(address indexed recipient, uint256 amount);
event Pings(uint256 indexed pingCounter);
uint256 private _pingCounter;
uint256 private constant _maxPingCount = 999;
// https://github.com/opengsn/ctf-react/blob/main/workspaces/ctf-eth/contracts/CaptureTheFlag.sol
function _msgSender() internal view override(Context, ERC2771Context) returns (address sender) {
sender = ERC2771Context._msgSender();
}
// https://github.com/opengsn/ctf-react/blob/main/workspaces/ctf-eth/contracts/CaptureTheFlag.sol
function _msgData() internal view override(Context, ERC2771Context) returns (bytes calldata) {
return ERC2771Context._msgData();
}
function _contextSuffixLength() internal view override(Context, ERC2771Context) returns (uint256) {
return ERC2771Context._contextSuffixLength();
}
// https://solidity-by-example.org/signature/
function splitSignature(bytes memory sig) public pure returns (bytes32 r, bytes32 s, uint8 v) {
require(sig.length == 65, "invalid signature length");
assembly {
/*
First 32 bytes stores the length of the signature
add(sig, 32) = pointer of sig + 32
effectively, skips first 32 bytes of signature
mload(p) loads next 32 bytes starting at the memory address p into memory
*/
// first 32 bytes, after the length prefix
r := mload(add(sig, 32))
// second 32 bytes
s := mload(add(sig, 64))
// final byte (first byte of the next 32 bytes)
v := byte(0, mload(add(sig, 96)))
}
// implicitly return (r, s, v)
}
function batchTransferWithPermit(
address owner,
address token,
address[] calldata recipients,
uint256[] calldata amounts,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external nonReentrant onlyOwner {
require(recipients.length == amounts.length, "Mismatched arrays");
IERC20Permit erc20P = IERC20Permit(token);
IERC20 erc20 = IERC20(token);
uint256 totalAmount = 0;
for (uint256 i = 0; i < amounts.length; i++) {
totalAmount += amounts[i];
}
erc20P.permit(owner, address(this), totalAmount, deadline, v, r, s);
batchTransferExecute(erc20, owner, recipients, amounts);
emit BatchTransfer(owner, token, recipients.length);
}
function batchTransferExecute(
IERC20 erc20,
address owner,
address[] calldata recipients,
uint256[] calldata amounts
) internal {
for (uint256 i = 0; i < recipients.length; i++) {
erc20.safeTransferFrom(owner, recipients[i], amounts[i]);
emit TransferDetails(recipients[i], amounts[i]);
}
}
function batchTransferWithPermitSign(
address token,
address[] calldata recipients,
uint256[] calldata amounts,
uint256 deadline,
bytes memory signature
) external nonReentrant onlyOwner {
require(recipients.length == amounts.length, "Mismatched arrays");
IERC20Permit erc20P = IERC20Permit(token);
IERC20 erc20 = IERC20(token);
uint256 totalAmount = 0;
for (uint256 i = 0; i < amounts.length; i++) {
totalAmount += amounts[i];
}
// https://solidity-by-example.org/hacks/weth-permit/
(bytes32 r, bytes32 s, uint8 v) = splitSignature(signature);
erc20P.permit(_msgSender(), address(this), totalAmount, deadline, v, r, s);
batchTransferExecute(erc20, _msgSender(), recipients, amounts);
emit BatchTransfer(_msgSender(), token, recipients.length);
}
function batchTransfer(address token, address[] calldata recipients, uint256[] calldata amounts)
external
nonReentrant
{
require(recipients.length == amounts.length, "Mismatched arrays");
IERC20 erc20 = IERC20(token);
uint256 totalAmount = 0;
for (uint256 i = 0; i < amounts.length; i++) {
totalAmount += amounts[i];
}
require(erc20.allowance(_msgSender(), address(this)) >= totalAmount, "Insufficient allowance");
require(erc20.balanceOf(_msgSender()) >= totalAmount, "Insufficient balance");
for (uint256 i = 0; i < recipients.length; i++) {
require(erc20.transferFrom(_msgSender(), recipients[i], amounts[i]), "Transfer failed");
emit TransferDetails(recipients[i], amounts[i]);
}
emit BatchTransfer(_msgSender(), token, recipients.length);
}
function pings() external nonReentrant {
require(_pingCounter < _maxPingCount, "Max Pings Reached");
_pingCounter++;
emit Pings(_pingCounter);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment