Files
CurrenciCombo/docs/Adapter_Architecture_Spec.md

19 KiB

Adapter Architecture Specification

Overview

This document specifies the architecture for the hybrid adapter system that supports both DeFi protocols and Fiat/DTL (banking rails) connectors. It defines adapter interfaces, whitelist/blacklist mechanisms, protocol versioning, upgrade paths, and integration guides.


1. Adapter System Architecture

High-Level Design

┌─────────────────────────────────────────────────────────────┐
│                    Combo Builder UI                         │
│              (Drag & Drop Adapter Selection)                │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│              Adapter Registry Contract                      │
│  (Whitelist/Blacklist, Version Management)                  │
└──────────────┬──────────────────────────────┬───────────────┘
               │                              │
               ▼                              ▼
    ┌──────────────────┐          ┌──────────────────┐
    │  DeFi Adapters   │          │ Fiat/DTL Adapters│
    │                  │          │                  │
    │ • Uniswap V3     │          │ • ISO-20022 Pay │
    │ • Aave           │          │ • SWIFT MT       │
    │ • Compound       │          │ • SEPA          │
    │ • Bridge         │          │ • FedNow        │
    └──────────────────┘          └──────────────────┘
               │                              │
               ▼                              ▼
    ┌──────────────────┐          ┌──────────────────┐
    │  DeFi Protocols  │          │  Banking Rails   │
    │  (On-Chain)      │          │  (Off-Chain)      │
    └──────────────────┘          └──────────────────┘

2. Adapter Interface Contract

Base Interface: IAdapter

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IAdapter {
    /**
     * @notice Execute a step using this adapter
     * @param stepData Encoded step-specific parameters
     * @return success Whether execution succeeded
     * @return returnData Return data from execution
     */
    function executeStep(bytes calldata stepData) external returns (bool success, bytes memory returnData);

    /**
     * @notice Prepare phase for 2PC (optional, if supported)
     * @param stepData Encoded step parameters
     * @return prepared Whether preparation succeeded
     */
    function prepareStep(bytes calldata stepData) external returns (bool prepared);

    /**
     * @notice Get adapter metadata
     * @return name Adapter name
     * @return version Adapter version
     * @return adapterType Type (DEFI or FIAT_DTL)
     */
    function getMetadata() external view returns (string memory name, string memory version, AdapterType adapterType);

    /**
     * @notice Check if adapter supports a specific step type
     * @param stepType Step type to check
     * @return supported Whether step type is supported
     */
    function supportsStepType(StepType stepType) external view returns (bool supported);
}

enum AdapterType {
    DEFI,
    FIAT_DTL
}

enum StepType {
    BORROW,
    SWAP,
    REPAY,
    PAY,
    DEPOSIT,
    WITHDRAW,
    BRIDGE
}

DeFi Adapter Example: UniswapV3Adapter.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./IAdapter.sol";
import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";

contract UniswapV3Adapter is IAdapter {
    ISwapRouter public constant swapRouter = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
    
    function executeStep(bytes calldata stepData) external override returns (bool success, bytes memory returnData) {
        SwapParams memory params = abi.decode(stepData, (SwapParams));
        
        ISwapRouter.ExactInputSingleParams memory swapParams = ISwapRouter.ExactInputSingleParams({
            tokenIn: params.tokenIn,
            tokenOut: params.tokenOut,
            fee: params.fee,
            recipient: params.recipient,
            deadline: block.timestamp + 300,
            amountIn: params.amountIn,
            amountOutMinimum: params.amountOutMinimum,
            sqrtPriceLimitX96: 0
        });

        uint256 amountOut = swapRouter.exactInputSingle(swapParams);
        
        return (true, abi.encode(amountOut));
    }

    function prepareStep(bytes calldata) external pure override returns (bool) {
        // Uniswap doesn't support prepare phase
        return false;
    }

    function getMetadata() external pure override returns (string memory, string memory, AdapterType) {
        return ("Uniswap V3", "3.0.1", AdapterType.DEFI);
    }

    function supportsStepType(StepType stepType) external pure override returns (bool) {
        return stepType == StepType.SWAP;
    }

    struct SwapParams {
        address tokenIn;
        address tokenOut;
        uint24 fee;
        address recipient;
        uint256 amountIn;
        uint256 amountOutMinimum;
    }
}

Fiat/DTL Adapter Example: ISO20022PayAdapter.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./IAdapter.sol";

contract ISO20022PayAdapter is IAdapter {
    address public orchestrator;
    mapping(bytes32 => PaymentRequest) public pendingPayments;

    struct PaymentRequest {
        bytes32 planId;
        string beneficiaryIBAN;
        uint256 amount;
        string currency;
        bool executed;
    }

    function executeStep(bytes calldata stepData) external override returns (bool success, bytes memory returnData) {
        require(msg.sender == orchestrator, "Only orchestrator");

        PayParams memory params = abi.decode(stepData, (PayParams));
        
        // Store payment request for off-chain processing
        bytes32 requestId = keccak256(abi.encodePacked(params.planId, params.beneficiaryIBAN, params.amount));
        pendingPayments[requestId] = PaymentRequest({
            planId: params.planId,
            beneficiaryIBAN: params.beneficiaryIBAN,
            amount: params.amount,
            currency: params.currency,
            executed: false
        });

        // Emit event for off-chain orchestrator to process
        emit PaymentRequested(requestId, params.planId, params.beneficiaryIBAN, params.amount, params.currency);

        return (true, abi.encode(requestId));
    }

    function prepareStep(bytes calldata stepData) external override returns (bool) {
        // Fiat payments can support prepare phase (provisional ISO message)
        PayParams memory params = abi.decode(stepData, (PayParams));
        bytes32 requestId = keccak256(abi.encodePacked(params.planId, params.beneficiaryIBAN, params.amount));
        
        // Mark as prepared (provisional)
        pendingPayments[requestId].executed = false; // Not yet executed

        emit PaymentPrepared(requestId);
        return true;
    }

    function getMetadata() external pure override returns (string memory, string memory, AdapterType) {
        return ("ISO-20022 Pay", "1.2.0", AdapterType.FIAT_DTL);
    }

    function supportsStepType(StepType stepType) external pure override returns (bool) {
        return stepType == StepType.PAY;
    }

    function confirmPayment(bytes32 requestId, string memory isoMessageId) external {
        require(msg.sender == orchestrator, "Only orchestrator");
        PaymentRequest storage payment = pendingPayments[requestId];
        require(!payment.executed, "Already executed");

        payment.executed = true;
        emit PaymentConfirmed(requestId, isoMessageId);
    }

    event PaymentRequested(bytes32 indexed requestId, bytes32 indexed planId, string beneficiaryIBAN, uint256 amount, string currency);
    event PaymentPrepared(bytes32 indexed requestId);
    event PaymentConfirmed(bytes32 indexed requestId, string isoMessageId);

    struct PayParams {
        bytes32 planId;
        string beneficiaryIBAN;
        uint256 amount;
        string currency;
    }
}

3. Whitelist/Blacklist Mechanisms

On-Chain Registry (Smart Contract)

// Managed by AdapterRegistry contract (see Smart_Contract_Interfaces.md)
// - registerAdapter() - Register new adapter
// - whitelistAdapter() - Add to whitelist
// - blacklistAdapter() - Remove from whitelist
// - isWhitelisted() - Check whitelist status

Off-Chain API Filtering

// Backend API filters adapters based on:
// 1. On-chain whitelist status
// 2. User role/permissions
// 3. Compliance requirements
// 4. Geographic restrictions

GET /api/adapters?type=DEFI&whitelistedOnly=true&userId=user123

UI Filtering

// Frontend filters adapters based on:
// 1. User selection (All, DeFi, Fiat/DTL, Whitelisted Only)
// 2. Chain compatibility
// 3. Compliance requirements

const filteredAdapters = adapters.filter(adapter => {
  if (filter === 'DEFI') return adapter.type === 'DEFI';
  if (filter === 'FIAT_DTL') return adapter.type === 'FIAT_DTL';
  if (filter === 'WHITELISTED') return adapter.whitelisted;
  return true; // ALL
});

4. Protocol Versioning

Version String Format

Major.Minor.Patch
Example: "3.0.1", "1.2.0"

Version Management

On-Chain (Adapter Contract)

function getMetadata() external view returns (string memory, string memory, AdapterType) {
    return ("Uniswap V3", "3.0.1", AdapterType.DEFI);
}

Off-Chain (API/Registry)

{
  "id": "uniswap-v3",
  "name": "Uniswap V3",
  "version": "3.0.1",
  "type": "DEFI",
  "whitelisted": true,
  "deprecated": false,
  "replacedBy": null,
  "chainIds": [1, 137, 42161],
  "lastUpdated": "2025-01-15T00:00:00Z"
}

Version Upgrade Path

  1. Register New Version: Deploy new adapter contract with incremented version
  2. Register in AdapterRegistry: Call registerAdapter() with new address
  3. Whitelist New Version: Call whitelistAdapter() for new address
  4. Deprecate Old Version: Optionally blacklist old version
  5. Update UI: Frontend fetches latest version from registry

Breaking Changes

  • Major Version: Incompatible API changes (new interface required)
  • Minor Version: New features, backward compatible
  • Patch Version: Bug fixes, backward compatible

5. Upgrade Paths

  • Deploy new adapter contract
  • Register in AdapterRegistry
  • Whitelist new contract
  • Update frontend to use new address
  • Old adapter remains for existing plans

Option 2: Proxy Pattern (For Complex Adapters)

// Use Transparent Proxy or UUPS
// Allows upgrade without changing address
// Requires careful upgrade governance

Option 3: Adapter Factory Pattern

contract AdapterFactory {
    function createAdapter(string memory version) external returns (address) {
        // Deploy new adapter instance
        // Register automatically
        // Return address
    }
}

6. Integration Guide for Adding New Adapters

Step 1: Implement IAdapter Interface

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./IAdapter.sol";

contract MyNewAdapter is IAdapter {
    function executeStep(bytes calldata stepData) external override returns (bool, bytes memory) {
        // Implementation
    }

    function prepareStep(bytes calldata stepData) external override returns (bool) {
        // Implementation (optional)
    }

    function getMetadata() external pure override returns (string memory, string memory, AdapterType) {
        return ("My New Adapter", "1.0.0", AdapterType.DEFI);
    }

    function supportsStepType(StepType stepType) external pure override returns (bool) {
        return stepType == StepType.SWAP; // Example
    }
}

Step 2: Deploy Contract

# Deploy to target network
npx hardhat run scripts/deploy.js --network mainnet

Step 3: Register in AdapterRegistry

// Call from admin account
adapterRegistry.registerAdapter(
    myNewAdapterAddress,
    AdapterType.DEFI,
    "1.0.0",
    abi.encode(ipfsHash) // Metadata
);

Step 4: Register Codehash in NotaryRegistry

// Get codehash
bytes32 codeHash;
assembly {
    codeHash := extcodehash(myNewAdapterAddress)
}

// Register
notaryRegistry.registerCodeHash(myNewAdapterAddress, codeHash);

Step 5: Whitelist Adapter

// After security review
adapterRegistry.whitelistAdapter(myNewAdapterAddress);

Step 6: Update Backend API

// Add adapter to database/configuration
const adapter = {
  id: 'my-new-adapter',
  address: myNewAdapterAddress,
  type: 'DEFI',
  version: '1.0.0',
  whitelisted: true
};

await db.adapters.insert(adapter);

Step 7: Update Frontend

// Adapter should appear automatically via API
// If custom UI needed, add to adapter palette configuration

Step 8: Testing

  • Unit tests for adapter contract
  • Integration tests with ComboHandler
  • E2E tests in UI
  • Security audit (if handling significant funds)

7. DeFi Adapter Integration Examples

Aave Lending Adapter

contract AaveAdapter is IAdapter {
    IPool public constant aavePool = IPool(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2);

    function executeStep(bytes calldata stepData) external override returns (bool, bytes memory) {
        LendingParams memory params = abi.decode(stepData, (LendingParams));
        
        if (params.action == LendingAction.BORROW) {
            aavePool.borrow(params.asset, params.amount, 2, 0, msg.sender); // Variable rate
        } else if (params.action == LendingAction.REPAY) {
            aavePool.repay(params.asset, params.amount, 2, msg.sender);
        }
        
        return (true, "");
    }

    enum LendingAction { BORROW, REPAY, DEPOSIT, WITHDRAW }
    
    struct LendingParams {
        LendingAction action;
        address asset;
        uint256 amount;
    }
}

Bridge Adapter (Cross-Chain)

contract BridgeAdapter is IAdapter {
    function executeStep(bytes calldata stepData) external override returns (bool, bytes memory) {
        BridgeParams memory params = abi.decode(stepData, (BridgeParams));
        
        // Lock tokens on source chain
        // Emit event for bridge service
        emit BridgeRequest(params.token, params.amount, params.targetChain, params.recipient);
        
        return (true, "");
    }

    event BridgeRequest(address indexed token, uint256 amount, uint256 targetChain, address recipient);
    
    struct BridgeParams {
        address token;
        uint256 amount;
        uint256 targetChain;
        address recipient;
    }
}

8. Fiat/DTL Adapter Integration Examples

SWIFT MT Adapter

contract SWIFTAdapter is IAdapter {
    function executeStep(bytes calldata stepData) external override returns (bool, bytes memory) {
        SWIFTParams memory params = abi.decode(stepData, (SWIFTParams));
        
        // Store SWIFT message request
        bytes32 messageId = keccak256(abi.encodePacked(params.planId, params.beneficiary, params.amount));
        emit SWIFTMessageRequested(messageId, params.planId, params.beneficiary, params.amount);
        
        return (true, abi.encode(messageId));
    }

    event SWIFTMessageRequested(bytes32 indexed messageId, bytes32 indexed planId, string beneficiary, uint256 amount);
    
    struct SWIFTParams {
        bytes32 planId;
        string beneficiary;
        uint256 amount;
        string currency;
        string messageType; // MT103, MT202, etc.
    }
}

SEPA Adapter

contract SEPAAdapter is IAdapter {
    function executeStep(bytes calldata stepData) external override returns (bool, bytes memory) {
        SEPAParams memory params = abi.decode(stepData, (SEPAParams));
        
        bytes32 paymentId = keccak256(abi.encodePacked(params.planId, params.creditorIBAN, params.amount));
        emit SEPACreditTransferRequested(paymentId, params.planId, params.creditorIBAN, params.amount);
        
        return (true, abi.encode(paymentId));
    }

    event SEPACreditTransferRequested(bytes32 indexed paymentId, bytes32 indexed planId, string creditorIBAN, uint256 amount);
    
    struct SEPAParams {
        bytes32 planId;
        string creditorIBAN;
        string creditorName;
        uint256 amount;
        string currency;
        string remittanceInfo;
    }
}

9. Security Considerations

Adapter Validation

  1. Codehash Verification: Verify adapter codehash matches registered hash before execution
  2. Whitelist Check: Only execute whitelisted adapters
  3. Reentrancy Protection: Use ReentrancyGuard in handler contract
  4. Input Validation: Validate all step parameters before execution

Access Control

  1. Orchestrator-Only Execution: Only orchestrator can call adapter execute functions
  2. Admin Functions: Multi-sig required for whitelist/blacklist operations
  3. Timelock: Implement timelock for critical operations

Audit Requirements

  1. Security Audit: All adapters must pass security audit before whitelisting
  2. Code Review: Peer review required for adapter code
  3. Testing: Comprehensive test coverage required

10. Testing Requirements

Unit Tests

// Test adapter interface implementation
function testExecuteStep() public {
    // Test successful execution
    // Test failure cases
    // Test return data
}

function testPrepareStep() public {
    // Test prepare phase (if supported)
}

Integration Tests

// Test adapter with ComboHandler
function testAdapterInCombo() public {
    // Test adapter works in multi-step combo
    // Test step dependencies
    // Test error handling
}

E2E Tests

// Test adapter in full UI flow
describe('Uniswap V3 Adapter', () => {
  it('should execute swap in combo', async () => {
    // Build combo with Uniswap step
    // Execute combo
    // Verify results
  });
});

11. Best Practices

Adapter Design

  1. Keep It Simple: Adapters should do one thing well
  2. Error Handling: Return clear error messages
  3. Gas Optimization: Minimize gas usage
  4. Event Emission: Emit events for off-chain tracking

Version Management

  1. Semantic Versioning: Follow semver (Major.Minor.Patch)
  2. Backward Compatibility: Maintain backward compatibility when possible
  3. Deprecation Policy: Clearly communicate deprecation timeline

Documentation

  1. README: Document adapter purpose, parameters, usage
  2. API Docs: Document all functions and parameters
  3. Examples: Provide usage examples

Document Version: 1.0
Last Updated: 2025-01-15
Author: Architecture Team