Files
CurrenciCombo/contracts/ComboHandler.sol
defiQUG f52313e7c6 Enhance ComboHandler and orchestrator functionality with access control and error handling improvements
- 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.
2025-11-05 17:55:48 -08:00

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
}
}