19 KiB
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