Skip to content

Commit

Permalink
feat: modify batcher contract to support erc20 sendMany
Browse files Browse the repository at this point in the history
Ticket: COIN-2782
  • Loading branch information
kamleshmugdiya committed Jan 30, 2025
1 parent d64bb5a commit aec9d8d
Show file tree
Hide file tree
Showing 2 changed files with 440 additions and 16 deletions.
100 changes: 85 additions & 15 deletions contracts/Batcher.sol
Original file line number Diff line number Diff line change
@@ -1,38 +1,50 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.20;

import '@openzeppelin/contracts/access/Ownable2Step.sol';
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';

// SPDX-License-Identifier: Apache-2.0
error EmptyRecipientsList();
error UnequalRecipientsAndValues();
error TooManyRecipients(uint256 provided, uint256 limit);
error TokenTransferFailed(address token, address from, address to, uint256 amount);

/**
*
* Batcher
* =======
*
* Contract that can take a batch of transfers, presented in the form of a recipients array and a values array, and
* funnel off those funds to the correct accounts in a single transaction. This is useful for saving on gas when a
* bunch of funds need to be transferred to different accounts.
*
* If more ETH is sent to `batch` than it is instructed to transfer, then the entire transaction will revert
* If any tokens are accidentally transferred to this account, contact the contract owner in order to recover them.
*
*/
/// @title Batcher - Batch transfer contract for ETH and ERC20 tokens
/// @notice Allows batch transfers of ETH and ERC20 tokens to multiple recipients in a single transaction
/// @dev Implements reentrancy protection and configurable gas limits for transfers

contract Batcher is Ownable2Step {
using SafeERC20 for IERC20;

event BatchTransfer(address sender, address recipient, uint256 value);
event TransferGasLimitChange(
uint256 prevTransferGasLimit,
uint256 newTransferGasLimit
);
event BatchTransferLimitChange(
uint256 prevBatchTransferLimit,
uint256 newBatchTransferLimit
);

uint256 public lockCounter;
uint256 public transferGasLimit;
uint256 public batchTransferLimit;

constructor(uint256 _transferGasLimit) Ownable(msg.sender) {
/// @notice Contract constructor
/// @param _transferGasLimit Gas limit for individual transfers
/// @param _batchTransferLimit Maximum number of transfers allowed in a batch
/// @dev Sets initial values for transfer limits and initializes the reentrancy guard
constructor(uint256 _transferGasLimit, uint256 _batchTransferLimit) Ownable(msg.sender) {
lockCounter = 1;
transferGasLimit = _transferGasLimit;
batchTransferLimit = _batchTransferLimit;
emit TransferGasLimitChange(0, transferGasLimit);
emit BatchTransferLimitChange(0, batchTransferLimit);
}

/// @notice Prevents reentrancy attacks
/// @dev Increments a counter before execution and checks it hasn't changed after
modifier lockCall() {
lockCounter++;
uint256 localCounter = lockCounter;
Expand All @@ -47,6 +59,11 @@ contract Batcher is Ownable2Step {
* The recipient with index i in recipients array will be sent values[i].
* Thus, recipients and values must be the same length
*/
/// @notice Batch transfer ETH to multiple recipients
/// @param recipients Array of recipient addresses
/// @param values Array of ETH amounts to send to each recipient
/// @dev Total value sent must match msg.value exactly
/// @dev Reverts if any transfer fails or if arrays are mismatched
function batch(address[] calldata recipients, uint256[] calldata values)
external
payable
Expand Down Expand Up @@ -78,12 +95,48 @@ contract Batcher is Ownable2Step {
require(totalSent == msg.value, 'Total sent out must equal total received');
}

/**
* @dev Batch transferFrom for an ERC20 token.
* @param token The address of the ERC20 token contract.
* @param recipients The array of recipient addresses.
* @param amounts The array of amounts to transfer to each recipient.
* Requirements:
* - `recipients` and `amounts` must have the same length.
* - The caller must have approved the contract to spend the tokens being transferred.
*/
/// @notice Batch transfer ERC20 tokens from sender to multiple recipients
/// @param token Address of the ERC20 token contract
/// @param recipients Array of recipient addresses
/// @param amounts Array of token amounts to transfer to each recipient
/// @dev Requires prior approval for token spending
/// @dev Uses SafeERC20 for secure token transfers
function batchTransferFrom(
address token,
address[] calldata recipients,
uint256[] calldata amounts
) external lockCall {
if (recipients.length == 0) revert EmptyRecipientsList();
if (recipients.length != amounts.length) revert UnequalRecipientsAndValues();
if (recipients.length > batchTransferLimit) revert TooManyRecipients(recipients.length, batchTransferLimit);

IERC20 safeToken = IERC20(token);
for (uint16 i = 0; i < recipients.length; i++) {
safeToken.safeTransferFrom(msg.sender, recipients[i], amounts[i]);
}
}

/**
* Recovery function for the contract owner to recover any ERC20 tokens or ETH that may get lost in the control of this contract.
* @param to The recipient to send to
* @param value The ETH value to send with the call
* @param data The data to send along with the call
*/
/// @notice Recover any ETH or tokens accidentally sent to the contract
/// @param to Destination address for recovery
/// @param value Amount of ETH to send
/// @param data Calldata for the recovery transaction
/// @return bytes The return data from the recovery call
/// @dev Only callable by contract owner
function recover(
address to,
uint256 value,
Expand All @@ -100,6 +153,10 @@ contract Batcher is Ownable2Step {
* a new amount of gas for an internal send to complete.
* @param newTransferGasLimit The new gas limit to send along with batched transfers
*/
/// @notice Update the gas limit for individual transfers
/// @param newTransferGasLimit New gas limit value
/// @dev Minimum value of 2300 required for basic ETH transfers
/// @dev Only callable by contract owner
function changeTransferGasLimit(uint256 newTransferGasLimit)
external
onlyOwner
Expand All @@ -109,6 +166,19 @@ contract Batcher is Ownable2Step {
transferGasLimit = newTransferGasLimit;
}

/// @notice Update the maximum number of transfers allowed in a batch
/// @param newBatchTransferLimit New maximum batch size
/// @dev Must be greater than zero
/// @dev Only callable by contract owner
function changeBatchTransferLimit(uint256 newBatchTransferLimit)
external
onlyOwner
{
require(newBatchTransferLimit > 0, 'Batch transfer limit too low');
emit BatchTransferLimitChange(batchTransferLimit, newBatchTransferLimit);
batchTransferLimit = newBatchTransferLimit;
}

fallback() external payable {
revert('Invalid fallback');
}
Expand Down
Loading

0 comments on commit aec9d8d

Please sign in to comment.