// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; /** * @title PacaBotManager * @dev A multicall contract that acts as a botOnly abstraction for PACA contracts * @notice This contract allows the owner to execute multiple bot-only functions across different PACA contracts */ contract PacaBotManager is Ownable, ReentrancyGuard { // Events event CallExecuted(address indexed target, bytes4 indexed selector, bool success); event MultiCallExecuted(uint256 callCount, uint256 successCount); // Errors error CallFailed(uint256 callIndex, address target, bytes data, bytes returnData); error EmptyCallData(); error InvalidTarget(); struct Call { address target; bytes callData; } struct CallResult { bool success; bytes returnData; } constructor() Ownable(msg.sender) { // Deployer is automatically the owner } /** * @notice Execute a single call to a target contract * @param target The contract address to call * @param callData The encoded function call data * @return success Whether the call succeeded * @return returnData The return data from the call */ function executeCall( address target, bytes calldata callData ) external onlyOwner nonReentrant returns (bool success, bytes memory returnData) { if (target == address(0)) revert InvalidTarget(); if (callData.length == 0) revert EmptyCallData(); (success, returnData) = target.call(callData); // Emit event (selector extraction simplified for testing) emit CallExecuted(target, bytes4(0), success); return (success, returnData); } /** * @notice Execute multiple calls atomically - all must succeed or all revert * @param calls Array of Call structs containing target and callData * @return results Array of CallResult structs with success status and return data */ function multiCallAtomic(Call[] calldata calls) external onlyOwner nonReentrant returns (CallResult[] memory results) { uint256 length = calls.length; results = new CallResult[](length); for (uint256 i = 0; i < length; i++) { if (calls[i].target == address(0)) revert InvalidTarget(); if (calls[i].callData.length == 0) revert EmptyCallData(); (bool success, bytes memory returnData) = calls[i].target.call(calls[i].callData); if (!success) { revert CallFailed(i, calls[i].target, calls[i].callData, returnData); } results[i] = CallResult({ success: success, returnData: returnData }); // Emit event (selector extraction simplified for testing) emit CallExecuted(calls[i].target, bytes4(0), success); } emit MultiCallExecuted(length, length); return results; } /** * @notice Execute multiple calls with partial success allowed * @param calls Array of Call structs containing target and callData * @return results Array of CallResult structs with success status and return data */ function multiCallPartial(Call[] calldata calls) external onlyOwner nonReentrant returns (CallResult[] memory results) { uint256 length = calls.length; results = new CallResult[](length); uint256 successCount = 0; for (uint256 i = 0; i < length; i++) { if (calls[i].target == address(0) || calls[i].callData.length == 0) { results[i] = CallResult({ success: false, returnData: abi.encode("Invalid target or empty call data") }); continue; } (bool success, bytes memory returnData) = calls[i].target.call(calls[i].callData); results[i] = CallResult({ success: success, returnData: returnData }); if (success) { successCount++; } // Emit event (selector extraction simplified for testing) emit CallExecuted(calls[i].target, bytes4(0), success); } emit MultiCallExecuted(length, successCount); return results; } /** * @notice Convenience function to clear stakes for a user on a specific PACA contract * @param pacaContract The PACA contract address * @param user The user whose stakes will be cleared */ function clearStakes(address pacaContract, address user) external onlyOwner nonReentrant { bytes memory callData = abi.encodeWithSignature("clearStakes(address)", user); (bool success, bytes memory returnData) = pacaContract.call(callData); if (!success) { revert CallFailed(0, pacaContract, callData, returnData); } emit CallExecuted(pacaContract, bytes4(0), success); } /** * @notice Convenience function to clear vestings for a user on a specific PACA contract * @param pacaContract The PACA contract address * @param user The user whose vestings will be cleared */ function clearVesting(address pacaContract, address user) external onlyOwner nonReentrant { bytes memory callData = abi.encodeWithSignature("clearVesting(address)", user); (bool success, bytes memory returnData) = pacaContract.call(callData); if (!success) { revert CallFailed(0, pacaContract, callData, returnData); } emit CallExecuted(pacaContract, bytes4(0), success); } /** * @notice Convenience function to clear withdraw stakes for a user on a specific PACA contract * @param pacaContract The PACA contract address * @param user The user whose withdraw stakes will be cleared */ function clearWithdrawStakes(address pacaContract, address user) external onlyOwner nonReentrant { bytes memory callData = abi.encodeWithSignature("clearWithdrawStakes(address)", user); (bool success, bytes memory returnData) = pacaContract.call(callData); if (!success) { revert CallFailed(0, pacaContract, callData, returnData); } emit CallExecuted(pacaContract, bytes4(0), success); } /** * @notice Emergency function to recover any accidentally sent ETH */ function emergencyWithdrawETH() external onlyOwner { payable(owner()).transfer(address(this).balance); } /** * @notice Emergency function to recover any accidentally sent ERC20 tokens * @param token The token contract address * @param amount The amount to recover */ function emergencyWithdrawToken(address token, uint256 amount) external onlyOwner { // Simple transfer call - assumes standard ERC20 (bool success,) = token.call( abi.encodeWithSignature("transfer(address,uint256)", owner(), amount) ); require(success, "Token transfer failed"); } }