-
Notifications
You must be signed in to change notification settings - Fork 61
/
Copy pathBatcher.sol
161 lines (141 loc) · 5.86 KB
/
Batcher.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
// 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';
error EmptyRecipientsList();
error UnequalRecipientsAndValues();
error TooManyRecipients(uint256 provided, uint256 limit);
error TokenTransferFailed(address token, address from, address to, uint256 amount);
/// @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;
/// @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');
}
/// @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
lockCall
{
require(recipients.length != 0, 'Must send to at least one person');
require(
recipients.length == values.length,
'Unequal recipients and values'
);
require(recipients.length < 256, 'Too many recipients');
uint256 totalSent = 0;
// Try to send all given amounts to all given recipients
// Revert everything if any transfer fails
for (uint8 i = 0; i < recipients.length; i++) {
require(recipients[i] != address(0), 'Invalid recipient address');
emit BatchTransfer(msg.sender, recipients[i], values[i]);
(bool success, ) = recipients[i].call{
value: values[i],
gas: transferGasLimit
}('');
require(success, 'Send failed');
totalSent += values[i];
}
require(totalSent == msg.value, 'Total sent out must equal total received');
}
/// @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,
bytes calldata data
) external onlyOwner returns (bytes memory) {
(bool success, bytes memory returnData) = to.call{ value: value }(data);
require(success, 'Recover failed');
return returnData;
}
/// @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
{
require(newTransferGasLimit >= 2300, 'Transfer gas limit too low');
emit TransferGasLimitChange(transferGasLimit, newTransferGasLimit);
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');
}
receive() external payable {
revert('Invalid receive');
}
}