19 KiB
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
- Register New Version: Deploy new adapter contract with incremented version
- Register in AdapterRegistry: Call
registerAdapter()with new address - Whitelist New Version: Call
whitelistAdapter()for new address - Deprecate Old Version: Optionally blacklist old version
- 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
Option 1: New Contract Deployment (Recommended)
- 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
- Codehash Verification: Verify adapter codehash matches registered hash before execution
- Whitelist Check: Only execute whitelisted adapters
- Reentrancy Protection: Use ReentrancyGuard in handler contract
- Input Validation: Validate all step parameters before execution
Access Control
- Orchestrator-Only Execution: Only orchestrator can call adapter execute functions
- Admin Functions: Multi-sig required for whitelist/blacklist operations
- Timelock: Implement timelock for critical operations
Audit Requirements
- Security Audit: All adapters must pass security audit before whitelisting
- Code Review: Peer review required for adapter code
- 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
- Keep It Simple: Adapters should do one thing well
- Error Handling: Return clear error messages
- Gas Optimization: Minimize gas usage
- Event Emission: Emit events for off-chain tracking
Version Management
- Semantic Versioning: Follow semver (Major.Minor.Patch)
- Backward Compatibility: Maintain backward compatibility when possible
- Deprecation Policy: Clearly communicate deprecation timeline
Documentation
- README: Document adapter purpose, parameters, usage
- API Docs: Document all functions and parameters
- Examples: Provide usage examples
Document Version: 1.0
Last Updated: 2025-01-15
Author: Architecture Team