Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions dashboard/app/lib/oracle-staleness.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { calculateOracleStaleness } from './oracle-staleness.ts';
import type { HistoryEntry } from './contract.ts';


describe('calculateOracleStaleness', () => {
it('returns zeros for empty history', () => {
const summary = calculateOracleStaleness([]);
assert.deepEqual(summary, {
assessedRuns: 0,
staleRuns: 0,
stalePercentage: 0,
totalRuns: 0,
});
});

it('correctly calculates staleness across ok runs', () => {
const history: HistoryEntry[] = [
{
status: 'ok',
safetyScore: 80,
methodologyVersion: 2,
computedAt: '2026-08-16T12:00:00Z',
runAt: '2026-08-16T12:00:00Z',
factors: {
oracleSafety: {
value: 100,
weight: 0.25,
detail: 'fresh',
components: [{ id: 'priceFreshness', label: 'Freshness', value: 100, detail: 'current' }],
},
},
},
{
status: 'ok',
safetyScore: 40,
methodologyVersion: 2,
computedAt: '2026-08-16T11:00:00Z',
runAt: '2026-08-16T11:00:00Z',
factors: {
oracleSafety: {
value: 20,
weight: 0.25,
detail: 'stale prices',
components: [{ id: 'priceFreshness', label: 'Freshness', value: 0, detail: 'stale' }],
},
},
},
{
status: 'failed',
error: 'network error',
runAt: '2026-08-16T10:00:00Z',
},
];

const summary = calculateOracleStaleness(history);
assert.equal(summary.totalRuns, 3);
assert.equal(summary.assessedRuns, 2);
assert.equal(summary.staleRuns, 1);
assert.equal(summary.stalePercentage, 50);
});

it('correctly tracks high-staleness scenario (e.g. K2 93% stale runs)', () => {
const history: HistoryEntry[] = [];
// 93 stale runs
for (let i = 0; i < 93; i++) {
history.push({
status: 'ok',
safetyScore: 30,
methodologyVersion: 2,
computedAt: new Date(1760000000000 + i * 300000).toISOString(),
runAt: new Date(1760000000000 + i * 300000).toISOString(),
factors: {
oracleSafety: {
value: 0,
weight: 0.25,
detail: 'dead prices',
components: [{ id: 'priceFreshness', label: 'Freshness', value: 0, detail: 'dead' }],
},
},
});
}
// 7 fresh runs
for (let i = 0; i < 7; i++) {
history.push({
status: 'ok',
safetyScore: 90,
methodologyVersion: 2,
computedAt: new Date(1760000000000 + (93 + i) * 300000).toISOString(),
runAt: new Date(1760000000000 + (93 + i) * 300000).toISOString(),
factors: {
oracleSafety: {
value: 100,
weight: 0.25,
detail: 'fresh prices',
components: [{ id: 'priceFreshness', label: 'Freshness', value: 100, detail: 'fresh' }],
},
},
});
}

const summary = calculateOracleStaleness(history);
assert.equal(summary.assessedRuns, 100);
assert.equal(summary.staleRuns, 93);
assert.equal(summary.stalePercentage, 93);
});
});
56 changes: 56 additions & 0 deletions dashboard/app/lib/oracle-staleness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { HistoryEntry } from '../app/lib/contract';

export interface OracleStalenessSummary {
/** Total runs with a valid factor breakdown in the historical set */
assessedRuns: number;
/** Number of runs where the oracle was detected as stale (score < 50 or priceFreshness < 50) */
staleRuns: number;
/** Percentage of observed runs where the oracle was stale (0–100) */
stalePercentage: number;
/** Total runs evaluated in history */
totalRuns: number;
}

/**
* Derives oracle staleness metrics across historical runs.
*
* Reuses the canonical oracleSafety factor from each historical run:
* A run's oracle is considered stale when:
* 1. An 'ok' run evaluates oracleSafety with a value < 50, OR
* 2. The priceFreshness component sub-score is < 50.
*
* Runs that don't assess oracleSafety (e.g. Dex protocols without an oracle) are safely excluded.
*/
export function calculateOracleStaleness(history: HistoryEntry[]): OracleStalenessSummary {
let assessedRuns = 0;
let staleRuns = 0;

for (const entry of history) {
if (entry.status !== 'ok') continue;

const oracleFactor = entry.factors?.oracleSafety;
if (!oracleFactor) continue;

assessedRuns += 1;

// Check if the priceFreshness component sub-score or overall factor score is degraded (< 50)
const freshnessComp = oracleFactor.components?.find((c) => c.id === 'priceFreshness');
const isFreshnessStale =
freshnessComp && freshnessComp.value !== null ? freshnessComp.value < 50 : false;
const isOverallFactorStale = oracleFactor.value < 50;

if (isFreshnessStale || isOverallFactorStale) {
staleRuns += 1;
}
}

const stalePercentage =
assessedRuns > 0 ? Math.round((staleRuns / assessedRuns) * 100) : 0;

return {
assessedRuns,
staleRuns,
stalePercentage,
totalRuns: history.length,
};
}
4 changes: 4 additions & 0 deletions dashboard/app/protocol/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { FactorBreakdown } from '../../../components/factor-breakdown';
import { Reveal, RevealGroup, RevealItem } from '../../../components/reveal';
import { RunHistory } from '../../../components/run-history';
import { ScoreHistoryChart } from '../../../components/score-history-chart';
import { OracleStalenessView } from '../../../components/oracle-staleness-view';

export const dynamic = 'force-dynamic';

Expand Down Expand Up @@ -94,6 +95,9 @@ export default async function ProtocolDetailPage({ params }: { params: Promise<{
)}

<FreshnessNotice detail={detail} />
<Reveal delay={0.09}>
<OracleStalenessView history={detail.history} />
</Reveal>

<ExternalRefs detail={detail} />

Expand Down
101 changes: 101 additions & 0 deletions dashboard/components/oracle-staleness-view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
'use client';

import { ActivitySquare, AlertTriangle, CheckCircle2 } from 'lucide-react';
import type { HistoryEntry } from '../app/lib/contract';
import { calculateOracleStaleness } from '../app/lib/oracle-staleness';
import { cn } from '../app/lib/cn';

export interface OracleStalenessViewProps {
history: HistoryEntry[];
className?: string;
}

/**
* Historical oracle-staleness view.
*
* Displays how often a protocol's oracle feeds have been stale over the
* observed run history (percentage of runs stale, stale vs assessed counts,
* and a status tone indicating frequency of stale periods).
*
* Visually distinct from the current-state status pill / badge:
* this component represents the historical aggregate behavior over time.
*/
export function OracleStalenessView({ history, className }: OracleStalenessViewProps) {
const summary = calculateOracleStaleness(history);

// If no runs assessed oracle safety (e.g. Dex without oracle, or zero history), omit component cleanly
if (summary.assessedRuns === 0) {
return null;
}

const { assessedRuns, staleRuns, stalePercentage } = summary;

// Stale frequency severity thresholds
const isHighStale = stalePercentage >= 50;
const isModerateStale = stalePercentage > 0 && stalePercentage < 50;
const isClean = stalePercentage === 0;

return (
<div
className={cn(
'mt-4 rounded-xl border border-line surface-lit p-4 transition-colors',
isHighStale && 'border-warn/40 bg-warn/5',
className,
)}
aria-label={`Historical oracle staleness: ${stalePercentage}% of runs stale`}
>
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2">
{isHighStale ? (
<AlertTriangle className="h-4 w-4 text-warn" aria-hidden="true" />
) : isModerateStale ? (
<ActivitySquare className="h-4 w-4 text-accent" aria-hidden="true" />
) : (
<CheckCircle2 className="h-4 w-4 text-safe" aria-hidden="true" />
)}
<span className="font-medium text-sm text-ink">Historical oracle staleness</span>
</div>

<div className="flex items-center gap-2">
<span
className={cn(
'score-num font-semibold text-sm',
isHighStale ? 'text-warn' : isModerateStale ? 'text-accent' : 'text-safe',
)}
>
{stalePercentage}% stale
</span>
<span className="text-xs text-muted">
({staleRuns} of {assessedRuns} observed runs)
</span>
</div>
</div>

<div className="mt-3">
{/* Visual progress bar representing the proportion of historical runs stale */}
<div className="h-1.5 w-full overflow-hidden rounded-full bg-line-soft">
<div
className={cn(
'h-full rounded-full transition-all duration-500',
isHighStale ? 'bg-warn' : isModerateStale ? 'bg-accent' : 'bg-safe',
)}
style={{ width: `${Math.max(4, stalePercentage)}%` }}
role="progressbar"
aria-valuenow={stalePercentage}
aria-valuemin={0}
aria-valuemax={100}
aria-label="Oracle staleness percentage"
/>
</div>
</div>

<p className="mt-2 text-xs leading-relaxed text-faint">
{isClean
? 'Oracle prices remained fresh across all evaluated historical runs.'
: isHighStale
? `Oracle prices were stale in ${staleRuns} of the last ${assessedRuns} runs (${stalePercentage}% of time). Persistent staleness indicates feed refresh gaps or unaligned TTLs.`
: `Oracle prices experienced intermittent staleness in ${staleRuns} of ${assessedRuns} runs (${stalePercentage}%).`}
</p>
</div>
);
}
Loading