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 83e6911
Show file tree
Hide file tree
Showing 2 changed files with 431 additions and 35 deletions.
110 changes: 76 additions & 34 deletions contracts/Batcher.sol
Original file line number Diff line number Diff line change
@@ -1,52 +1,62 @@
// 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;
_;
require(localCounter == lockCounter, 'Reentrancy attempt detected');
}

/**
* Transfer funds in a batch to each of recipients
* @param recipients The list of recipients to send to
* @param values The list of values to send to recipients.
* 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 The list of recipients to send to
/// @param values The list of values to send to recipients.
/// @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 +88,33 @@ contract Batcher is Ownable2Step {
require(totalSent == msg.value, 'Total sent out must equal total received');
}

/**
* 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 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]);
}
}

/// @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 @@ -94,12 +125,10 @@ contract Batcher is Ownable2Step {
return returnData;
}

/**
* Change the gas limit that is sent along with batched transfers.
* This is intended to protect against any EVM level changes that would require
* 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 +138,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 83e6911

Please sign in to comment.