- Added AccessControl to ComboHandler for role-based access management. - Implemented gas estimation for plan execution and improved gas limit checks. - Updated execution and preparation methods to enforce step count limits and role restrictions. - Enhanced error handling in orchestrator API endpoints with AppError for better validation feedback. - Integrated request timeout middleware for improved request management. - Updated Swagger documentation to reflect new API structure and parameters.
257 lines
9.0 KiB
Solidity
257 lines
9.0 KiB
Solidity
// 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
|
|
}
|
|
}
|