// Risk Control Service // Enforces hard caps and validates deal parameters import { Decimal } from '@prisma/client/runtime/library'; import { logger } from '@/infrastructure/monitoring/logger'; import { DEFAULT_RISK_PARAMS } from './config'; import { RiskCheckResult, CapitalBuckets, DealExecutionRequest } from './types'; export class RiskControlService { async validateDealRequest( request: DealExecutionRequest, currentNav: Decimal ): Promise { const errors: string[] = []; const maxLtv = new Decimal(request.maxLtv ?? DEFAULT_RISK_PARAMS.MAX_LTV); const maxUsdtzExposure = currentNav.mul( DEFAULT_RISK_PARAMS.MAX_USDTZ_EXPOSURE_PCT ); if (maxLtv.gt(DEFAULT_RISK_PARAMS.MAX_LTV)) { errors.push( `Requested LTV ${maxLtv.toString()} exceeds maximum ${DEFAULT_RISK_PARAMS.MAX_LTV}` ); } const totalEth = new Decimal(request.totalEthValue); const workingLiquidity = totalEth.mul(0.30); const estimatedBorrow = workingLiquidity.mul(maxLtv); const estimatedUsdtzPurchase = estimatedBorrow.div( new Decimal(1).minus( new Decimal(request.usdtzDiscountRate ?? DEFAULT_RISK_PARAMS.DEFAULT_USDTZ_DISCOUNT) ) ); if (estimatedUsdtzPurchase.gt(maxUsdtzExposure)) { errors.push( `Estimated USDTz exposure ${estimatedUsdtzPurchase.toString()} exceeds maximum ${maxUsdtzExposure.toString()}` ); } return { passed: errors.length === 0, maxLtv: new Decimal(DEFAULT_RISK_PARAMS.MAX_LTV), maxUsdtzExposure, totalNav: currentNav, errors, }; } async checkLtvCompliance( collateralValue: Decimal, borrowAmount: Decimal, maxLtv: Decimal = new Decimal(DEFAULT_RISK_PARAMS.MAX_LTV) ): Promise { const errors: string[] = []; const ltv = borrowAmount.div(collateralValue); if (ltv.gt(maxLtv)) { errors.push( `LTV ${ltv.mul(100).toFixed(2)}% exceeds maximum ${maxLtv.mul(100).toFixed(2)}%` ); } logger.info('LTV Check', { collateralValue: collateralValue.toString(), borrowAmount: borrowAmount.toString(), ltv: ltv.mul(100).toFixed(2) + '%', maxLtv: maxLtv.mul(100).toFixed(2) + '%', passed: errors.length === 0, }); return { passed: errors.length === 0, ltv, maxLtv, errors, }; } async checkUsdtzExposure( usdtzAmount: Decimal, totalNav: Decimal ): Promise { const errors: string[] = []; const maxExposure = totalNav.mul(DEFAULT_RISK_PARAMS.MAX_USDTZ_EXPOSURE_PCT); if (usdtzAmount.gt(maxExposure)) { errors.push( `USDTz exposure ${usdtzAmount.toString()} exceeds maximum ${maxExposure.toString()}` ); } return { passed: errors.length === 0, usdtzExposure: usdtzAmount, maxUsdtzExposure: maxExposure, totalNav, errors, }; } async validateNoRehypothecation( usdtzAmount: Decimal, collateralAssets: string[] ): Promise { const errors: string[] = []; if (collateralAssets.includes('USDTz')) { errors.push('USDTz cannot be used as collateral (no rehypothecation)'); } return { passed: errors.length === 0, errors, }; } } export const riskControlService = new RiskControlService();