Files
CurrenciCombo/docs/Smart_Contract_Interfaces.md

760 lines
22 KiB
Markdown

# 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