// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; import "./interfaces/IComboHandler.sol"; import "./interfaces/IAdapterRegistry.sol"; import "./interfaces/INotaryRegistry.sol"; /** * @title ComboHandler * @notice Aggregates multiple DeFi protocol calls and DLT operations into atomic transactions * @dev Implements 2PC pattern, proper signature verification, access control, and gas optimization */ contract ComboHandler is IComboHandler, Ownable, ReentrancyGuard, AccessControl { using ECDSA for bytes32; bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE"); IAdapterRegistry public immutable adapterRegistry; INotaryRegistry public immutable notaryRegistry; mapping(bytes32 => ExecutionState) public executions; struct ExecutionState { ExecutionStatus status; uint256 currentStep; Step[] steps; bool prepared; address creator; uint256 gasLimit; } event PlanExecuted(bytes32 indexed planId, bool success, uint256 gasUsed); event PlanPrepared(bytes32 indexed planId, address indexed creator); event PlanCommitted(bytes32 indexed planId); event PlanAborted(bytes32 indexed planId, string reason); event StepExecuted(bytes32 indexed planId, uint256 stepIndex, bool success, uint256 gasUsed); constructor(address _adapterRegistry, address _notaryRegistry) { require(_adapterRegistry != address(0), "Invalid adapter registry"); require(_notaryRegistry != address(0), "Invalid notary registry"); adapterRegistry = IAdapterRegistry(_adapterRegistry); notaryRegistry = INotaryRegistry(_notaryRegistry); _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); } /** * @notice Execute a multi-step combo plan atomically * @param planId Unique identifier for the execution plan * @param steps Array of step configurations * @param signature User's cryptographic signature on the plan * @return success Whether execution completed successfully * @return receipts Array of transaction receipts for each step */ function executeCombo( bytes32 planId, Step[] calldata steps, bytes calldata signature ) external override nonReentrant returns (bool success, StepReceipt[] memory receipts) { require(executions[planId].status == ExecutionStatus.PENDING, "Plan already executed"); require(steps.length > 0 && steps.length <= 20, "Invalid step count"); // Verify signature using ECDSA bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", keccak256(abi.encodePacked(planId, steps, msg.sender)))); address signer = messageHash.recover(signature); require(signer == msg.sender, "Invalid signature"); // Register with notary notaryRegistry.registerPlan(planId, steps, msg.sender); uint256 gasStart = gasleft(); uint256 estimatedGas = _estimateGas(steps); executions[planId] = ExecutionState({ status: ExecutionStatus.IN_PROGRESS, currentStep: 0, steps: steps, prepared: false, creator: msg.sender, gasLimit: estimatedGas }); receipts = new StepReceipt[](steps.length); // Execute steps sequentially for (uint256 i = 0; i < steps.length; i++) { uint256 stepGasStart = gasleft(); // Check gas limit require(gasleft() > 100000, "Insufficient gas"); (bool stepSuccess, bytes memory returnData, uint256 gasUsed) = _executeStep(steps[i], i); receipts[i] = StepReceipt({ stepIndex: i, success: stepSuccess, returnData: returnData, gasUsed: stepGasStart - gasleft() }); emit StepExecuted(planId, i, stepSuccess, gasUsed); if (!stepSuccess) { executions[planId].status = ExecutionStatus.FAILED; notaryRegistry.finalizePlan(planId, false); revert("Step execution failed"); } } executions[planId].status = ExecutionStatus.COMPLETE; success = true; uint256 totalGasUsed = gasStart - gasleft(); emit PlanExecuted(planId, true, totalGasUsed); // Finalize with notary notaryRegistry.finalizePlan(planId, true); } /** * @notice Prepare phase for 2PC (two-phase commit) * @param planId Plan identifier * @param steps Execution steps * @return prepared Whether all steps are prepared */ function prepare( bytes32 planId, Step[] calldata steps ) external override onlyRole(EXECUTOR_ROLE) returns (bool prepared) { require(executions[planId].status == ExecutionStatus.PENDING, "Plan not pending"); require(steps.length > 0 && steps.length <= 20, "Invalid step count"); // Validate all steps can be prepared for (uint256 i = 0; i < steps.length; i++) { require(_canPrepareStep(steps[i]), "Step cannot be prepared"); } executions[planId] = ExecutionState({ status: ExecutionStatus.IN_PROGRESS, currentStep: 0, steps: steps, prepared: true, creator: msg.sender, gasLimit: _estimateGas(steps) }); emit PlanPrepared(planId, msg.sender); prepared = true; } /** * @notice Commit phase for 2PC * @param planId Plan identifier * @return committed Whether commit was successful */ function commit(bytes32 planId) external override onlyRole(EXECUTOR_ROLE) returns (bool committed) { ExecutionState storage state = executions[planId]; require(state.prepared, "Plan not prepared"); require(state.status == ExecutionStatus.IN_PROGRESS, "Invalid state"); // Execute all prepared steps for (uint256 i = 0; i < state.steps.length; i++) { (bool success, , ) = _executeStep(state.steps[i], i); require(success, "Commit failed"); } state.status = ExecutionStatus.COMPLETE; committed = true; emit PlanCommitted(planId); notaryRegistry.finalizePlan(planId, true); } /** * @notice Abort phase for 2PC (rollback) * @param planId Plan identifier */ function abort(bytes32 planId) external override { ExecutionState storage state = executions[planId]; require(state.status == ExecutionStatus.IN_PROGRESS, "Cannot abort"); require(msg.sender == state.creator || hasRole(EXECUTOR_ROLE, msg.sender), "Not authorized"); // Release any reserved funds/collateral _rollbackSteps(planId); state.status = ExecutionStatus.ABORTED; emit PlanAborted(planId, "User aborted"); notaryRegistry.finalizePlan(planId, false); } /** * @notice Get execution status for a plan */ function getExecutionStatus(bytes32 planId) external view override returns (ExecutionStatus) { return executions[planId].status; } /** * @notice Estimate gas for plan execution */ function _estimateGas(Step[] memory steps) internal pure returns (uint256) { // Rough estimation: 100k per step + 50k overhead return steps.length * 100000 + 50000; } /** * @notice Execute a single step * @dev Internal function with gas tracking and optimization */ function _executeStep(Step memory step, uint256 stepIndex) internal returns (bool success, bytes memory returnData, uint256 gasUsed) { // Verify adapter is whitelisted require(adapterRegistry.isWhitelisted(step.target), "Adapter not whitelisted"); uint256 gasBefore = gasleft(); // Check gas limit require(gasleft() > 100000, "Insufficient gas"); (success, returnData) = step.target.call{value: step.value, gas: gasleft() - 50000}( abi.encodeWithSignature("executeStep(bytes)", step.data) ); gasUsed = gasBefore - gasleft(); // Emit event for step execution if (!success && returnData.length > 0) { // Log failure reason if available } } /** * @notice Check if step can be prepared */ function _canPrepareStep(Step memory step) internal view returns (bool) { // Check if adapter supports prepare phase return adapterRegistry.isWhitelisted(step.target); } /** * @notice Rollback steps on abort */ function _rollbackSteps(bytes32 planId) internal { ExecutionState storage state = executions[planId]; // Release reserved funds, unlock collateral, etc. // Implementation depends on specific step types // For now, just mark as aborted } }