# Smart Contract Interface Specifications ## Overview This document defines the smart contract interfaces for the ISO-20022 Combo Flow system, including handler contracts for atomic execution, notary registry for codehash tracking, adapter registry for whitelisting, and integration patterns for atomicity (2PC, HTLC, conditional finality). --- ## 1. Handler/Aggregator Contract Interface ### Purpose The handler contract aggregates multiple DeFi protocol calls and DLT operations into a single atomic transaction. It executes steps sequentially, passing outputs between steps, and ensures atomicity across the entire workflow. ### Interface: `IComboHandler` ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; interface IComboHandler { /** * @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 returns (bool success, StepReceipt[] memory receipts); /** * @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 returns (bool prepared); /** * @notice Commit phase for 2PC * @param planId Plan identifier * @return committed Whether commit was successful */ function commit(bytes32 planId) external returns (bool committed); /** * @notice Abort phase for 2PC (rollback) * @param planId Plan identifier */ function abort(bytes32 planId) external; /** * @notice Get execution status for a plan * @param planId Plan identifier * @return status Execution status (PENDING, IN_PROGRESS, COMPLETE, FAILED, ABORTED) */ function getExecutionStatus(bytes32 planId) external view returns (ExecutionStatus status); } struct Step { StepType stepType; bytes data; // Encoded step-specific parameters address target; // Target contract address (adapter or protocol) uint256 value; // ETH value to send (if applicable) } enum StepType { BORROW, SWAP, REPAY, PAY, DEPOSIT, WITHDRAW, BRIDGE } enum ExecutionStatus { PENDING, IN_PROGRESS, COMPLETE, FAILED, ABORTED } struct StepReceipt { uint256 stepIndex; bool success; bytes returnData; uint256 gasUsed; } ``` ### Implementation Example: `ComboHandler.sol` ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "./IComboHandler.sol"; import "./IAdapterRegistry.sol"; import "./INotaryRegistry.sol"; contract ComboHandler is IComboHandler, Ownable, ReentrancyGuard { IAdapterRegistry public adapterRegistry; INotaryRegistry public notaryRegistry; mapping(bytes32 => ExecutionState) public executions; struct ExecutionState { ExecutionStatus status; uint256 currentStep; Step[] steps; bool prepared; } constructor(address _adapterRegistry, address _notaryRegistry) { adapterRegistry = IAdapterRegistry(_adapterRegistry); notaryRegistry = INotaryRegistry(_notaryRegistry); } 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"); // Verify signature require(_verifySignature(planId, signature, msg.sender), "Invalid signature"); // Register with notary notaryRegistry.registerPlan(planId, steps, msg.sender); executions[planId] = ExecutionState({ status: ExecutionStatus.IN_PROGRESS, currentStep: 0, steps: steps, prepared: false }); receipts = new StepReceipt[](steps.length); // Execute steps sequentially for (uint256 i = 0; i < steps.length; i++) { (bool stepSuccess, bytes memory returnData, uint256 gasUsed) = _executeStep(steps[i], i); receipts[i] = StepReceipt({ stepIndex: i, success: stepSuccess, returnData: returnData, gasUsed: gasUsed }); if (!stepSuccess) { executions[planId].status = ExecutionStatus.FAILED; revert("Step execution failed"); } } executions[planId].status = ExecutionStatus.COMPLETE; success = true; // Finalize with notary notaryRegistry.finalizePlan(planId, true); } function prepare( bytes32 planId, Step[] calldata steps ) external override returns (bool prepared) { require(executions[planId].status == ExecutionStatus.PENDING, "Plan not pending"); // 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 }); prepared = true; } function commit(bytes32 planId) external override 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; notaryRegistry.finalizePlan(planId, true); } function abort(bytes32 planId) external override { ExecutionState storage state = executions[planId]; require(state.status == ExecutionStatus.IN_PROGRESS, "Cannot abort"); // Release any reserved funds/collateral _rollbackSteps(planId); state.status = ExecutionStatus.ABORTED; notaryRegistry.finalizePlan(planId, false); } function getExecutionStatus(bytes32 planId) external view override returns (ExecutionStatus) { return executions[planId].status; } 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(); (success, returnData) = step.target.call{value: step.value}( abi.encodeWithSignature("executeStep(bytes)", step.data) ); gasUsed = gasBefore - gasleft(); } function _canPrepareStep(Step memory step) internal view returns (bool) { // Check if adapter supports prepare phase // Implementation depends on adapter interface return true; } function _rollbackSteps(bytes32 planId) internal { // Release reserved funds, unlock collateral, etc. // Implementation depends on specific step types } function _verifySignature(bytes32 planId, bytes calldata signature, address signer) internal pure returns (bool) { // Verify ECDSA signature bytes32 messageHash = keccak256(abi.encodePacked(planId)); bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); address recovered = ecrecover(ethSignedMessageHash, v, r, s); return recovered == signer; } } ``` --- ## 2. Notary Registry Contract Interface ### Purpose The notary registry contract stores codehashes, plan attestations, and provides immutable audit trails for compliance and non-repudiation. ### Interface: `INotaryRegistry` ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; interface INotaryRegistry { /** * @notice Register a new execution plan * @param planId Unique plan identifier * @param steps Execution steps * @param creator Plan creator address * @return proofHash Cryptographic proof hash */ function registerPlan( bytes32 planId, Step[] calldata steps, address creator ) external returns (bytes32 proofHash); /** * @notice Finalize a plan execution (success or failure) * @param planId Plan identifier * @param success Whether execution succeeded */ function finalizePlan(bytes32 planId, bool success) external; /** * @notice Register adapter codehash for security * @param adapter Address of adapter contract * @param codeHash Hash of adapter contract bytecode */ function registerCodeHash(address adapter, bytes32 codeHash) external; /** * @notice Verify adapter codehash matches registered hash * @param adapter Adapter address * @return matches Whether codehash matches */ function verifyCodeHash(address adapter) external view returns (bool matches); /** * @notice Get notary proof for a plan * @param planId Plan identifier * @return proof Notary proof structure */ function getProof(bytes32 planId) external view returns (NotaryProof memory proof); /** * @notice Get all plans registered by a creator * @param creator Creator address * @return planIds Array of plan IDs */ function getPlansByCreator(address creator) external view returns (bytes32[] memory planIds); } struct NotaryProof { bytes32 planId; bytes32 proofHash; address creator; uint256 registeredAt; uint256 finalizedAt; bool finalized; bool success; bytes32[] stepHashes; } ``` ### Implementation Example: `NotaryRegistry.sol` ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/Ownable.sol"; import "./INotaryRegistry.sol"; contract NotaryRegistry is INotaryRegistry, Ownable { mapping(bytes32 => NotaryProof) public proofs; mapping(address => bytes32[]) public creatorPlans; mapping(address => bytes32) public codeHashes; event PlanRegistered(bytes32 indexed planId, address creator, bytes32 proofHash); event PlanFinalized(bytes32 indexed planId, bool success); event CodeHashRegistered(address indexed adapter, bytes32 codeHash); function registerPlan( bytes32 planId, Step[] calldata steps, address creator ) external override returns (bytes32 proofHash) { require(proofs[planId].planId == bytes32(0), "Plan already registered"); bytes32[] memory stepHashes = new bytes32[](steps.length); for (uint256 i = 0; i < steps.length; i++) { stepHashes[i] = keccak256(abi.encode(steps[i])); } bytes32 stepsHash = keccak256(abi.encode(stepHashes)); proofHash = keccak256(abi.encodePacked(planId, creator, stepsHash, block.timestamp)); proofs[planId] = NotaryProof({ planId: planId, proofHash: proofHash, creator: creator, registeredAt: block.timestamp, finalizedAt: 0, finalized: false, success: false, stepHashes: stepHashes }); creatorPlans[creator].push(planId); emit PlanRegistered(planId, creator, proofHash); } function finalizePlan(bytes32 planId, bool success) external override { NotaryProof storage proof = proofs[planId]; require(proof.planId != bytes32(0), "Plan not registered"); require(!proof.finalized, "Plan already finalized"); proof.finalized = true; proof.success = success; proof.finalizedAt = block.timestamp; emit PlanFinalized(planId, success); } function registerCodeHash(address adapter, bytes32 codeHash) external override onlyOwner { codeHashes[adapter] = codeHash; emit CodeHashRegistered(adapter, codeHash); } function verifyCodeHash(address adapter) external view override returns (bool matches) { bytes32 registeredHash = codeHashes[adapter]; if (registeredHash == bytes32(0)) return false; bytes32 currentHash; assembly { currentHash := extcodehash(adapter) } return currentHash == registeredHash; } function getProof(bytes32 planId) external view override returns (NotaryProof memory) { return proofs[planId]; } function getPlansByCreator(address creator) external view override returns (bytes32[] memory) { return creatorPlans[creator]; } } ``` --- ## 3. Adapter Registry Contract Interface ### Purpose The adapter registry manages whitelisting/blacklisting of adapters (both DeFi protocols and Fiat/DTL connectors), tracks versions, and enforces upgrade controls. ### Interface: `IAdapterRegistry` ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; interface IAdapterRegistry { /** * @notice Check if adapter is whitelisted * @param adapter Adapter contract address * @return whitelisted Whether adapter is whitelisted */ function isWhitelisted(address adapter) external view returns (bool whitelisted); /** * @notice Register a new adapter * @param adapter Adapter contract address * @param adapterType Type of adapter (DEFI or FIAT_DTL) * @param version Adapter version string * @param metadata Additional metadata (IPFS hash, etc.) */ function registerAdapter( address adapter, AdapterType adapterType, string calldata version, bytes calldata metadata ) external; /** * @notice Whitelist an adapter * @param adapter Adapter contract address */ function whitelistAdapter(address adapter) external; /** * @notice Blacklist an adapter * @param adapter Adapter contract address */ function blacklistAdapter(address adapter) external; /** * @notice Get adapter information * @param adapter Adapter contract address * @return info Adapter information structure */ function getAdapterInfo(address adapter) external view returns (AdapterInfo memory info); /** * @notice List all whitelisted adapters * @param adapterType Filter by type (0 = ALL) * @return adapters Array of adapter addresses */ function listAdapters(AdapterType adapterType) external view returns (address[] memory adapters); } enum AdapterType { ALL, DEFI, FIAT_DTL } struct AdapterInfo { address adapter; AdapterType adapterType; string version; bool whitelisted; bool blacklisted; uint256 registeredAt; bytes metadata; } ``` ### Implementation Example: `AdapterRegistry.sol` ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/Ownable.sol"; import "./IAdapterRegistry.sol"; contract AdapterRegistry is IAdapterRegistry, Ownable { mapping(address => AdapterInfo) public adapters; address[] public adapterList; event AdapterRegistered(address indexed adapter, AdapterType adapterType, string version); event AdapterWhitelisted(address indexed adapter); event AdapterBlacklisted(address indexed adapter); function registerAdapter( address adapter, AdapterType adapterType, string calldata version, bytes calldata metadata ) external override onlyOwner { require(adapters[adapter].adapter == address(0), "Adapter already registered"); adapters[adapter] = AdapterInfo({ adapter: adapter, adapterType: adapterType, version: version, whitelisted: false, blacklisted: false, registeredAt: block.timestamp, metadata: metadata }); adapterList.push(adapter); emit AdapterRegistered(adapter, adapterType, version); } function whitelistAdapter(address adapter) external override onlyOwner { require(adapters[adapter].adapter != address(0), "Adapter not registered"); require(!adapters[adapter].blacklisted, "Adapter is blacklisted"); adapters[adapter].whitelisted = true; emit AdapterWhitelisted(adapter); } function blacklistAdapter(address adapter) external override onlyOwner { require(adapters[adapter].adapter != address(0), "Adapter not registered"); adapters[adapter].blacklisted = true; adapters[adapter].whitelisted = false; emit AdapterBlacklisted(adapter); } function isWhitelisted(address adapter) external view override returns (bool) { AdapterInfo memory info = adapters[adapter]; return info.whitelisted && !info.blacklisted; } function getAdapterInfo(address adapter) external view override returns (AdapterInfo memory) { return adapters[adapter]; } function listAdapters(AdapterType adapterType) external view override returns (address[] memory) { uint256 count = 0; for (uint256 i = 0; i < adapterList.length; i++) { if (adapterType == AdapterType.ALL || adapters[adapterList[i]].adapterType == adapterType) { if (adapters[adapterList[i]].whitelisted && !adapters[adapterList[i]].blacklisted) { count++; } } } address[] memory result = new address[](count); uint256 index = 0; for (uint256 i = 0; i < adapterList.length; i++) { if (adapterType == AdapterType.ALL || adapters[adapterList[i]].adapterType == adapterType) { if (adapters[adapterList[i]].whitelisted && !adapters[adapterList[i]].blacklisted) { result[index] = adapterList[i]; index++; } } } return result; } } ``` --- ## 4. Integration Patterns for Atomicity ### Pattern A: Two-Phase Commit (2PC) ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract TwoPhaseCommitHandler { enum Phase { PREPARE, COMMIT, ABORT } mapping(bytes32 => Phase) public phases; function prepare(bytes32 planId, Step[] calldata steps) external { // Mark assets as reserved // Store prepare state phases[planId] = Phase.PREPARE; } function commit(bytes32 planId) external { require(phases[planId] == Phase.PREPARE, "Not prepared"); // Execute all steps atomically phases[planId] = Phase.COMMIT; } function abort(bytes32 planId) external { require(phases[planId] == Phase.PREPARE, "Not prepared"); // Release reserved assets phases[planId] = Phase.ABORT; } } ``` ### Pattern B: HTLC-like Pattern ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract HTLCPattern { struct HTLC { bytes32 hashLock; address beneficiary; uint256 amount; uint256 expiry; bool claimed; } mapping(bytes32 => HTLC) public htlcLocks; function createHTLC( bytes32 planId, bytes32 hashLock, address beneficiary, uint256 amount, uint256 expiry ) external { htlcLocks[planId] = HTLC({ hashLock: hashLock, beneficiary: beneficiary, amount: amount, expiry: expiry, claimed: false }); } function claimHTLC(bytes32 planId, bytes32 preimage) external { HTLC storage htlc = htlcLocks[planId]; require(keccak256(abi.encodePacked(preimage)) == htlc.hashLock, "Invalid preimage"); require(block.timestamp < htlc.expiry, "Expired"); require(!htlc.claimed, "Already claimed"); htlc.claimed = true; // Transfer funds } } ``` ### Pattern C: Conditional Finality via Notary ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract ConditionalFinalityHandler { INotaryRegistry public notaryRegistry; mapping(bytes32 => bool) public pendingFinalization; function executeWithConditionalFinality( bytes32 planId, Step[] calldata steps ) external { // Execute DLT steps // Mark as pending finalization pendingFinalization[planId] = true; // Notary must co-sign after bank settlement } function finalizeWithNotary(bytes32 planId, bytes calldata notarySignature) external { require(pendingFinalization[planId], "Not pending"); require(notaryRegistry.verifyNotarySignature(planId, notarySignature), "Invalid notary signature"); // Complete finalization pendingFinality[planId] = false; } } ``` --- ## 5. Security Considerations ### Access Control - Use OpenZeppelin's `Ownable` or `AccessControl` for admin functions - Implement multi-sig for critical operations (adapter whitelisting, codehash registration) ### Reentrancy Protection - Use `ReentrancyGuard` for execute functions - Follow checks-effects-interactions pattern ### Upgradeability - Consider using proxy patterns (Transparent/UUPS) for upgradeable contracts - Implement timelocks for upgrades - Require multi-sig for upgrade approvals ### Codehash Verification - Register codehashes for all adapters - Verify codehash before execution - Prevent execution if codehash doesn't match ### Gas Optimization - Batch operations where possible - Use `calldata` instead of `memory` for arrays - Minimize storage operations --- ## 6. Testing Requirements ### Unit Tests - Test each interface function - Test error cases (invalid inputs, unauthorized access) - Test atomicity (all-or-nothing execution) ### Integration Tests - Test full workflow execution - Test 2PC prepare/commit/abort flows - Test notary integration - Test adapter registry whitelisting ### Fuzz Tests - Fuzz step configurations - Fuzz plan structures - Fuzz edge cases (empty steps, large arrays) --- **Document Version**: 1.0 **Last Updated**: 2025-01-15 **Author**: Smart Contract Team