Files
CurrenciCombo/docs/Smart_Contract_Interfaces.md

22 KiB

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

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

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

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

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

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

// 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)

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

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

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