SPB Git

Repository Public

risk.ts 3.9 KB 45 lines typescript

1import { assetKey, type AssetRef, type RiskAssessment, type RiskFlag, type SwapQuote } from './types.ts';23/** USD value of an amount, or null when the asset has no trusted price. Display/ranking only. */4export type Valuer = (asset: AssetRef, amount: bigint) => number | null;56export interface Valuation { inUsd: number | null; outUsd: number | null; minOutUsd: number | null; extraFeesUsd: number; unpricedFees: number; netUsd: number | null }78/** Value a quote: guaranteed output minus the fees that are not already deducted from it. */9export function valueQuote(q: SwapQuote, value: Valuer): Valuation {10  const inUsd = value(q.request.from, q.request.amountIn), outUsd = value(q.request.to, q.amountOut), minOutUsd = value(q.request.to, q.minAmountOut);11  let extraFeesUsd = 0, unpricedFees = 0;12  for (const f of q.fees) { if (f.includedInOutput) continue; const v = value(f.asset, f.amount); if (v === null) unpricedFees += 1; else extraFeesUsd += v; }13  return { inUsd, outUsd, minOutUsd, extraFeesUsd, unpricedFees, netUsd: minOutUsd === null ? null : minOutUsd - extraFeesUsd };14}1516const order = { low: 0, medium: 1, high: 2 } as const;17/**18 * Mandatory, mode-independent checks. A quote that is far better than the market is treated as more suspicious than19 * one that is worse: providers do not give money away, but a tampered or mis-decimalled answer looks exactly like that.20 */21export function assessRisk(q: SwapQuote, v: Valuation, peers: SwapQuote[] = []): RiskAssessment {22  const flags: RiskFlag[] = [];23  const add = (code: string, level: RiskFlag['level'], detail?: string) => flags.push({ code, level, detail });24  if (q.minAmountOut <= 0n || q.amountOut <= 0n || q.minAmountOut > q.amountOut) add('inconsistent_amounts', 'high');25  if (q.executability.class === 'quote_only') add('not_executable', 'medium', q.executability.reason);26  if (q.executability.class === 'opaque') add('opaque_transaction', 'medium', q.executability.reason);27  if (q.priceImpactBps !== null) { if (q.priceImpactBps >= 300) add('high_price_impact', 'high', `${q.priceImpactBps} bps`); else if (q.priceImpactBps >= 100) add('price_impact', 'medium', `${q.priceImpactBps} bps`); }28  if (q.steps.some((s) => s.kind === 'bridge' || s.kind === 'native_swap')) { add('cross_chain', 'medium'); if (q.refundable === false) { if (q.recoverable) add('recoverable_not_refundable', 'medium'); else add('no_refund_path', 'high'); } if (q.refundable === null) add('refund_unknown', 'medium'); }29  if (!q.atomic && q.steps.length > 1) add('multi_step', 'medium', `${q.steps.length} steps`);30  if (q.estimatedSeconds === null) add('no_time_estimate', 'low');31  if (v.unpricedFees > 0) add('fee_unpriced', 'low', `${v.unpricedFees} fee item(s) without a price`);32  if (v.inUsd !== null && v.outUsd !== null && v.inUsd > 0) {33    const ratio = v.outUsd / v.inUsd;34    if (ratio > 1.03) add('better_than_market', 'high', `output worth ${(ratio * 100).toFixed(1)} % of input`);35    else if (ratio < 0.9) add('far_below_market', 'high', `output worth ${(ratio * 100).toFixed(1)} % of input`);36    else if (ratio < 0.97) add('below_market', 'medium', `output worth ${(ratio * 100).toFixed(1)} % of input`);37  } else add('no_market_reference', 'medium');38  // disagreement between providers on the same pair: an answer far above every peer is not trusted39  const same = peers.filter((p) => p.id !== q.id && assetKey(p.request.to) === assetKey(q.request.to) && p.request.amountIn === q.request.amountIn).map((p) => p.amountOut).sort((a, b) => (a < b ? -1 : 1));40  if (same.length >= 2) { const med = same[Math.floor(same.length / 2)]!; if (med > 0n && q.amountOut * 100n > med * 105n) add('outlier_vs_peers', 'high', 'more than 5 % above the median of other providers'); }41  const level = flags.reduce<RiskFlag['level']>((m, f) => (order[f.level] > order[m] ? f.level : m), 'low');42  const score = Math.min(100, flags.reduce((s, f) => s + (f.level === 'high' ? 40 : f.level === 'medium' ? 12 : 3), 0));43  return { level, score, flags };44}45
Files main
Menu

Theme