Created
April 21, 2025 06:43
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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