Files
CurrenciCombo/docs/Simulation_Engine_Spec.md

19 KiB

Simulation Engine Specification

Overview

This document specifies the optional simulation engine for the ISO-20022 Combo Flow system. The simulation engine provides dry-run execution logic, gas estimation, slippage calculation, liquidity checks, failure prediction, and result presentation. It is toggleable for advanced users per requirement 2b.


1. Simulation Engine Architecture

High-Level Design

┌─────────────────────────────────────────────────────────────┐
│                    Combo Builder UI                         │
│              [Simulation Toggle: ON/OFF]                    │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│              Simulation Engine API                           │
│  POST /api/plans/{planId}/simulate                          │
└──────────────┬──────────────────────────────┬───────────────┘
               │                              │
               ▼                              ▼
    ┌──────────────────┐          ┌──────────────────┐
    │  DLT Simulator  │          │  Fiat Simulator  │
    │                  │          │                  │
    │ • Gas Estimation│          │ • Bank Routing    │
    │ • Slippage Calc  │          │ • Fee Calculation│
    │ • Liquidity Check│          │ • Settlement Time│
    └──────────────────┘          └──────────────────┘
               │                              │
               ▼                              ▼
    ┌──────────────────┐          ┌──────────────────┐
    │  Price Oracles   │          │  Bank APIs       │
    │  (On-Chain)      │          │  (Off-Chain)     │
    └──────────────────┘          └──────────────────┘

2. API Specification

Endpoint: POST /api/plans/{planId}/simulate

interface SimulationRequest {
  planId: string;
  options?: {
    includeGasEstimate?: boolean;      // Default: true
    includeSlippageAnalysis?: boolean; // Default: true
    includeLiquidityCheck?: boolean;    // Default: true
    includeBankRouting?: boolean;       // Default: true (for fiat steps)
    chainId?: number;                   // Default: current chain
  };
}

interface SimulationResponse {
  planId: string;
  status: 'SUCCESS' | 'FAILURE' | 'PARTIAL';
  steps: SimulationStepResult[];
  summary: {
    gasEstimate: number;
    estimatedCost: number; // USD
    totalSlippage: number; // Percentage
    executionTime: number;  // Seconds
  };
  slippageAnalysis: SlippageAnalysis;
  liquidityCheck: LiquidityCheck;
  warnings: string[];
  errors: string[];
  timestamp: string;
}

Response Structure

interface SimulationStepResult {
  stepIndex: number;
  stepType: 'borrow' | 'swap' | 'repay' | 'pay';
  status: 'SUCCESS' | 'FAILURE' | 'WARNING';
  message: string;
  estimatedOutput?: {
    token: string;
    amount: number;
  };
  gasEstimate?: number;
  slippage?: number;
  liquidityStatus?: 'SUFFICIENT' | 'INSUFFICIENT' | 'LOW';
  bankRouting?: {
    estimatedTime: number; // Minutes
    fee: number;
    currency: string;
  };
}

interface SlippageAnalysis {
  expectedSlippage: number;      // Percentage
  riskLevel: 'LOW' | 'MEDIUM' | 'HIGH';
  liquidityDepth: number;         // Total liquidity in pool
  priceImpact: number;            // Percentage
  warnings: string[];
}

interface LiquidityCheck {
  sufficient: boolean;
  poolDepth: number;
  requiredAmount: number;
  availableAmount: number;
  warnings: string[];
}

3. Dry-Run Execution Logic

Step-by-Step Simulation

class SimulationEngine {
  async simulatePlan(plan: Plan, options: SimulationOptions): Promise<SimulationResponse> {
    const results: SimulationStepResult[] = [];
    let cumulativeGas = 0;
    let totalSlippage = 0;
    const warnings: string[] = [];
    const errors: string[] = [];

    // Simulate each step sequentially
    for (let i = 0; i < plan.steps.length; i++) {
      const step = plan.steps[i];
      const stepResult = await this.simulateStep(step, i, plan, options);
      
      results.push(stepResult);
      
      if (stepResult.status === 'FAILURE') {
        errors.push(`Step ${i + 1} failed: ${stepResult.message}`);
        return {
          status: 'FAILURE',
          steps: results,
          errors,
          warnings
        };
      }

      if (stepResult.status === 'WARNING') {
        warnings.push(`Step ${i + 1}: ${stepResult.message}`);
      }

      cumulativeGas += stepResult.gasEstimate || 0;
      totalSlippage += stepResult.slippage || 0;
    }

    // Aggregate results
    return {
      status: 'SUCCESS',
      steps: results,
      summary: {
        gasEstimate: cumulativeGas,
        estimatedCost: this.calculateCost(cumulativeGas),
        totalSlippage,
        executionTime: this.estimateExecutionTime(plan)
      },
      slippageAnalysis: this.analyzeSlippage(results),
      liquidityCheck: this.checkLiquidity(results),
      warnings,
      errors: []
    };
  }

  async simulateStep(
    step: PlanStep,
    index: number,
    plan: Plan,
    options: SimulationOptions
  ): Promise<SimulationStepResult> {
    switch (step.type) {
      case 'borrow':
        return await this.simulateBorrow(step, index);
      case 'swap':
        return await this.simulateSwap(step, index, options);
      case 'repay':
        return await this.simulateRepay(step, index);
      case 'pay':
        return await this.simulatePay(step, index, options);
      default:
        return {
          stepIndex: index,
          stepType: step.type,
          status: 'FAILURE',
          message: 'Unknown step type'
        };
    }
  }
}

DeFi Step Simulation

async simulateSwap(
  step: SwapStep,
  index: number,
  options: SimulationOptions
): Promise<SimulationStepResult> {
  // 1. Get current price from oracle
  const currentPrice = await this.priceOracle.getPrice(step.from, step.to);
  
  // 2. Calculate slippage
  const slippage = await this.calculateSlippage(step.from, step.to, step.amount);
  
  // 3. Check liquidity
  const liquidity = await this.liquidityChecker.check(step.from, step.to, step.amount);
  
  // 4. Estimate gas
  const gasEstimate = await this.gasEstimator.estimateSwap(step.from, step.to, step.amount);
  
  // 5. Calculate expected output
  const expectedOutput = step.amount * currentPrice * (1 - slippage / 100);
  
  // 6. Validate minimum receive
  if (step.minRecv && expectedOutput < step.minRecv) {
    return {
      stepIndex: index,
      stepType: 'swap',
      status: 'FAILURE',
      message: `Expected output ${expectedOutput} is below minimum ${step.minRecv}`,
      estimatedOutput: { token: step.to, amount: expectedOutput },
      slippage,
      liquidityStatus: liquidity.status
    };
  }

  return {
    stepIndex: index,
    stepType: 'swap',
    status: liquidity.sufficient ? 'SUCCESS' : 'WARNING',
    message: liquidity.sufficient ? 'Swap would succeed' : 'Low liquidity warning',
    estimatedOutput: { token: step.to, amount: expectedOutput },
    gasEstimate,
    slippage,
    liquidityStatus: liquidity.status
  };
}

Fiat Step Simulation

async simulatePay(
  step: PayStep,
  index: number,
  options: SimulationOptions
): Promise<SimulationStepResult> {
  // 1. Validate IBAN
  if (!this.validateIBAN(step.beneficiary.IBAN)) {
    return {
      stepIndex: index,
      stepType: 'pay',
      status: 'FAILURE',
      message: 'Invalid IBAN format'
    };
  }

  // 2. Get bank routing info
  const routing = await this.bankRouter.getRouting(step.beneficiary.IBAN, step.asset);
  
  // 3. Calculate fees
  const fee = await this.feeCalculator.calculateFiatFee(step.amount, step.asset, routing);
  
  // 4. Estimate settlement time
  const settlementTime = await this.settlementEstimator.estimate(step.asset, routing);

  return {
    stepIndex: index,
    stepType: 'pay',
    status: 'SUCCESS',
    message: 'Payment would be processed',
    bankRouting: {
      estimatedTime: settlementTime,
      fee,
      currency: step.asset
    }
  };
}

4. Gas Estimation

Gas Estimation Strategy

class GasEstimator {
  async estimateSwap(tokenIn: string, tokenOut: string, amount: number): Promise<number> {
    // Base gas for swap
    const baseGas = 150000;
    
    // Additional gas for complex routing
    const routingGas = await this.estimateRoutingGas(tokenIn, tokenOut);
    
    // Gas for token approvals (if needed)
    const approvalGas = await this.estimateApprovalGas(tokenIn);
    
    return baseGas + routingGas + approvalGas;
  }

  async estimateBorrow(asset: string, amount: number): Promise<number> {
    // Base gas for borrow
    const baseGas = 200000;
    
    // Gas for collateral check
    const collateralGas = 50000;
    
    // Gas for LTV calculation
    const ltvGas = 30000;
    
    return baseGas + collateralGas + ltvGas;
  }

  async estimateFullPlan(plan: Plan): Promise<number> {
    let totalGas = 21000; // Base transaction gas
    
    for (const step of plan.steps) {
      switch (step.type) {
        case 'borrow':
          totalGas += await this.estimateBorrow(step.asset, step.amount);
          break;
        case 'swap':
          totalGas += await this.estimateSwap(step.from, step.to, step.amount);
          break;
        case 'repay':
          totalGas += 100000; // Standard repay gas
          break;
      }
    }
    
    // Add handler overhead
    totalGas += 50000;
    
    return totalGas;
  }

  calculateCost(gas: number, gasPrice: number): number {
    // gasPrice in gwei, convert to ETH then USD
    const ethCost = (gas * gasPrice * 1e9) / 1e18;
    const usdCost = ethCost * await this.getETHPrice();
    return usdCost;
  }
}

5. Slippage Calculation

Slippage Calculation Logic

class SlippageCalculator {
  async calculateSlippage(
    tokenIn: string,
    tokenOut: string,
    amountIn: number
  ): Promise<number> {
    // Get current pool reserves
    const reserves = await this.getPoolReserves(tokenIn, tokenOut);
    
    // Calculate price impact using constant product formula (x * y = k)
    const priceImpact = this.calculatePriceImpact(
      reserves.tokenIn,
      reserves.tokenOut,
      amountIn
    );
    
    // Add fixed fee (e.g., 0.3% for Uniswap)
    const protocolFee = 0.3;
    
    // Total slippage = price impact + protocol fee
    const totalSlippage = priceImpact + protocolFee;
    
    return totalSlippage;
  }

  calculatePriceImpact(
    reserveIn: number,
    reserveOut: number,
    amountIn: number
  ): number {
    // Constant product formula: (x + Δx) * (y - Δy) = x * y
    // Solving for Δy: Δy = (y * Δx) / (x + Δx)
    const amountOut = (reserveOut * amountIn) / (reserveIn + amountIn);
    const priceBefore = reserveOut / reserveIn;
    const priceAfter = (reserveOut - amountOut) / (reserveIn + amountIn);
    const priceImpact = ((priceBefore - priceAfter) / priceBefore) * 100;
    
    return priceImpact;
  }

  analyzeSlippage(results: SimulationStepResult[]): SlippageAnalysis {
    const swapSteps = results.filter(r => r.stepType === 'swap');
    const totalSlippage = swapSteps.reduce((sum, r) => sum + (r.slippage || 0), 0);
    const avgSlippage = totalSlippage / swapSteps.length;
    
    let riskLevel: 'LOW' | 'MEDIUM' | 'HIGH';
    if (avgSlippage < 0.5) {
      riskLevel = 'LOW';
    } else if (avgSlippage < 2.0) {
      riskLevel = 'MEDIUM';
    } else {
      riskLevel = 'HIGH';
    }

    const warnings: string[] = [];
    if (avgSlippage > 1.0) {
      warnings.push(`High slippage expected: ${avgSlippage.toFixed(2)}%`);
    }

    return {
      expectedSlippage: avgSlippage,
      riskLevel,
      liquidityDepth: 0, // Aggregate from steps
      priceImpact: avgSlippage,
      warnings
    };
  }
}

6. Liquidity Checks

Liquidity Check Logic

class LiquidityChecker {
  async check(
    tokenIn: string,
    tokenOut: string,
    amountIn: number
  ): Promise<LiquidityCheck> {
    // Get pool liquidity
    const pool = await this.getPool(tokenIn, tokenOut);
    const availableLiquidity = pool.reserveOut;
    
    // Calculate required output
    const price = await this.getPrice(tokenIn, tokenOut);
    const requiredOutput = amountIn * price;
    
    // Check if sufficient
    const sufficient = availableLiquidity >= requiredOutput * 1.1; // 10% buffer
    
    const warnings: string[] = [];
    if (!sufficient) {
      warnings.push(`Insufficient liquidity: need ${requiredOutput}, have ${availableLiquidity}`);
    } else if (availableLiquidity < requiredOutput * 1.5) {
      warnings.push(`Low liquidity: ${((availableLiquidity / requiredOutput) * 100).toFixed(1)}% buffer`);
    }

    return {
      sufficient,
      poolDepth: availableLiquidity,
      requiredAmount: requiredOutput,
      availableAmount: availableLiquidity,
      warnings
    };
  }
}

7. Failure Prediction

Failure Prediction Logic

class FailurePredictor {
  async predictFailures(plan: Plan): Promise<string[]> {
    const failures: string[] = [];

    // Check step dependencies
    for (let i = 0; i < plan.steps.length; i++) {
      const step = plan.steps[i];
      
      // Check if previous step outputs are sufficient
      if (i > 0) {
        const prevStep = plan.steps[i - 1];
        const prevOutput = await this.getStepOutput(prevStep);
        
        if (step.type === 'swap' && step.amount > prevOutput.amount) {
          failures.push(`Step ${i + 1}: Insufficient input from previous step`);
        }
      }

      // Check step-specific validations
      if (step.type === 'borrow') {
        const canBorrow = await this.checkBorrowCapacity(step.asset, step.amount);
        if (!canBorrow) {
          failures.push(`Step ${i + 1}: Cannot borrow ${step.amount} ${step.asset}`);
        }
      }

      if (step.type === 'pay') {
        const isValidIBAN = this.validateIBAN(step.beneficiary.IBAN);
        if (!isValidIBAN) {
          failures.push(`Step ${i + 1}: Invalid IBAN`);
        }
      }
    }

    // Check recursion depth
    const borrowCount = plan.steps.filter(s => s.type === 'borrow').length;
    if (borrowCount - 1 > plan.maxRecursion) {
      failures.push(`Recursion depth ${borrowCount - 1} exceeds maximum ${plan.maxRecursion}`);
    }

    // Check LTV
    const totalBorrowed = plan.steps
      .filter(s => s.type === 'borrow')
      .reduce((sum, s) => sum + (s as BorrowStep).amount, 0);
    const totalCollateral = await this.getTotalCollateral();
    const ltv = totalBorrowed / totalCollateral;
    
    if (ltv > plan.maxLTV) {
      failures.push(`LTV ${ltv} exceeds maximum ${plan.maxLTV}`);
    }

    return failures;
  }
}

8. Result Presentation Format

UI Presentation

// Simulation Results Component
const SimulationResults = ({ results }: { results: SimulationResponse }) => {
  return (
    <div className="simulation-results">
      <h2>Simulation Results</h2>
      
      {/* Status */}
      <StatusBadge status={results.status} />
      
      {/* Summary */}
      <div className="summary">
        <div>Gas Estimate: {results.summary.gasEstimate.toLocaleString()}</div>
        <div>Estimated Cost: ${results.summary.estimatedCost.toFixed(2)}</div>
        <div>Total Slippage: {results.summary.totalSlippage.toFixed(2)}%</div>
        <div>Execution Time: ~{results.summary.executionTime}s</div>
      </div>

      {/* Step-by-Step Results */}
      <div className="steps">
        {results.steps.map((step, i) => (
          <StepResultCard key={i} step={step} />
        ))}
      </div>

      {/* Warnings */}
      {results.warnings.length > 0 && (
        <WarningPanel warnings={results.warnings} />
      )}

      {/* Errors */}
      {results.errors.length > 0 && (
        <ErrorPanel errors={results.errors} />
      )}

      {/* Actions */}
      <div className="actions">
        <Button onClick={onRunAgain}>Run Simulation Again</Button>
        <Button onClick={onProceed} disabled={results.status === 'FAILURE'}>
          Proceed to Sign
        </Button>
      </div>
    </div>
  );
};

9. Optional Toggle Implementation

Frontend Toggle

// Builder UI with optional simulation toggle
const BuilderPage = () => {
  const [simulationEnabled, setSimulationEnabled] = useState(false);

  return (
    <div>
      {/* Summary Panel */}
      <SummaryPanel>
        <Checkbox
          checked={simulationEnabled}
          onChange={(e) => setSimulationEnabled(e.target.checked)}
          label="Enable Simulation (Advanced)"
        />
        
        {simulationEnabled && (
          <Button onClick={handleSimulate}>Simulate</Button>
        )}
      </SummaryPanel>
    </div>
  );
};

Backend Handling

// Backend respects simulation toggle
if (simulationEnabled && user.isAdvanced) {
  // Show simulation button
  // Allow simulation requests
} else {
  // Hide simulation button
  // Simulation still available via API for advanced users
}

10. Performance Requirements

Response Time

  • Simulation Time: < 5 seconds for typical workflows
  • Gas Estimation: < 1 second per step
  • Slippage Calculation: < 500ms per swap
  • Liquidity Check: < 1 second per check

Caching

  • Cache price oracle data for 30 seconds
  • Cache liquidity data for 10 seconds
  • Cache gas estimates for 60 seconds

11. Testing Requirements

Unit Tests

describe('SimulationEngine', () => {
  it('should simulate swap step', async () => {
    const result = await engine.simulateStep(swapStep, 0);
    expect(result.status).toBe('SUCCESS');
    expect(result.slippage).toBeLessThan(1.0);
  });

  it('should predict failures', async () => {
    const failures = await predictor.predictFailures(invalidPlan);
    expect(failures.length).toBeGreaterThan(0);
  });
});

Integration Tests

describe('Simulation API', () => {
  it('should return simulation results', async () => {
    const response = await api.simulatePlan(planId);
    expect(response.status).toBe('SUCCESS');
    expect(response.steps.length).toBe(plan.steps.length);
  });
});

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