Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
12 changes: 11 additions & 1 deletion src/lib/top-vaults/TopVaultsPage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@
totalVaultCount?: number;
/** Include blacklisted vaults in the listing summary stats. */
includeBlacklistedInStats?: boolean;
/** Exclude vaults above this TVL from listing summary TVL and TVL-weighted stats. */
maxSummaryTvlUsd?: number;
/** Do not visually strike through blacklisted vault rows. */
disableBlacklistedStrikethrough?: boolean;
}

let {
Expand Down Expand Up @@ -91,7 +95,9 @@
detailDescription,
beforeTable,
totalVaultCount,
includeBlacklistedInStats
includeBlacklistedInStats,
maxSummaryTvlUsd,
disableBlacklistedStrikethrough
}: Props = $props();

let renderDetailAsideInHero = $derived(
Expand Down Expand Up @@ -219,6 +225,8 @@
{defaultSort}
{defaultDirection}
{includeBlacklistedInStats}
{maxSummaryTvlUsd}
{disableBlacklistedStrikethrough}
/>
{:else if !topVaults?.vaults.length}
{#if stablecoinMetadata}
Expand All @@ -245,6 +253,8 @@
{defaultSort}
{defaultDirection}
{includeBlacklistedInStats}
{maxSummaryTvlUsd}
{disableBlacklistedStrikethrough}
/>
{/if}
</div>
Expand Down
27 changes: 20 additions & 7 deletions src/lib/top-vaults/TopVaultsTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@
totalVaultCount?: number;
/** Include blacklisted vaults in the listing summary stats. */
includeBlacklistedInStats?: boolean;
/** Exclude vaults above this TVL from listing summary TVL and TVL-weighted stats. */
maxSummaryTvlUsd?: number;
/** Do not visually strike through blacklisted vault rows. */
disableBlacklistedStrikethrough?: boolean;
}

const emptyTopVaults: TopVaults = {
Expand Down Expand Up @@ -147,7 +151,9 @@
defaultDirection,
loading = false,
totalVaultCount: totalVaultCountProp,
includeBlacklistedInStats = false
includeBlacklistedInStats = false,
maxSummaryTvlUsd,
disableBlacklistedStrikethrough = false
}: Props = $props();

// --- Sort column registry (key → compareFn + default direction) ---
Expand Down Expand Up @@ -491,15 +497,22 @@
includeBlacklistedInStats ? filteredVaults : filteredVaults.filter((v) => !isBlacklisted(v))
);

let statsVaultsWithTvl = $derived(
statsVaults.map((v) => ({
...v,
current_nav: getVaultCurrentTvlUsd(v)
}))
);

// Calculate total TVL from fully-filtered vaults
let totalTvl = $derived(calculateTotalTvl(statsVaults.map((v) => ({ current_nav: getVaultCurrentTvlUsd(v) }))));
let totalTvl = $derived(calculateTotalTvl(statsVaultsWithTvl, { maxTvlUsd: maxSummaryTvlUsd }));

// Calculate TVL-weighted average 1M APY from fully-filtered vaults
let avgTvlWeightedApy1M = $derived(
calculateTvlWeightedApy(
statsVaults.map((v) => ({ ...v, current_nav: getVaultCurrentTvlUsd(v) })),
{ includeBlacklisted: includeBlacklistedInStats }
)
calculateTvlWeightedApy(statsVaultsWithTvl, {
includeBlacklisted: includeBlacklistedInStats,
maxTvlUsd: maxSummaryTvlUsd
})
);

// sort vaults
Expand Down Expand Up @@ -1035,7 +1048,7 @@
{@const statusReason = [vault.deposit_closed_reason, vault.redemption_closed_reason]
.filter(Boolean)
.join('; ')}
<tr class={['targetable', blacklisted && 'blacklisted']}>
<tr class={['targetable', blacklisted && !disableBlacklistedStrikethrough && 'blacklisted']}>
<!-- index cell is populated with row index via `rowNumber` CSS counter -->
<td class="index"></td>
{#if showChainCol}
Expand Down
25 changes: 25 additions & 0 deletions src/lib/top-vaults/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
getVaultTvlNative,
isNonUsdDenominatedVault,
withVaultDenominationTokenRate,
calculateTotalTvl,
calculateTvlWeightedApy
} from './helpers';
import type { StablecoinMetadata } from '$lib/stablecoin-metadata/schemas';
Expand Down Expand Up @@ -134,6 +135,30 @@ describe('calculateTvlWeightedApy', () => {

expect(calculateTvlWeightedApy([included, blacklisted], { includeBlacklisted: true })).toBe(0.4);
});

test('excludes vaults above the max TVL from the weighted average', () => {
const included = createTestVault('Included vault', {
current_nav: 100_000,
one_month_cagr: 0.1
});
const abnormal = createTestVault('Abnormal TVL vault', {
current_nav: 2_000_000_000,
one_month_cagr: 1
});

expect(calculateTvlWeightedApy([included, abnormal], { maxTvlUsd: 1_000_000_000 })).toBe(0.1);
});
});

describe('calculateTotalTvl', () => {
test('excludes non-finite and above-threshold TVL values when requested', () => {
expect(
calculateTotalTvl(
[{ current_nav: 100_000 }, { current_nav: 2_000_000_000 }, { current_nav: Number.POSITIVE_INFINITY }],
{ maxTvlUsd: 1_000_000_000 }
)
).toBe(100_000);
});
});

describe('hasSupportedProtocol', () => {
Expand Down
19 changes: 16 additions & 3 deletions src/lib/top-vaults/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export const MIN_TVL_THRESHOLD = 10_000;

/** Default TVL threshold - global and per-chain */
export const DEFAULT_TVL_THRESHOLD = 50_000;
/** Maximum TVL value included in listing summary totals. Higher values are treated as broken scanner data. */
export const MAX_SUMMARY_TVL_USD = 1_000_000_000;
// prettier-ignore
const CHAIN_TVL_THRESHOLD_OVERRIDES = new Map([
[HYPERCORE_CHAIN_ID, 1_000_000]
Expand Down Expand Up @@ -556,6 +558,7 @@ type TvlWeightedApyVault = Pick<VaultInfo, 'current_nav' | 'one_month_cagr_net'

interface TvlWeightedApyOptions {
includeBlacklisted?: boolean;
maxTvlUsd?: number;
}

/**
Expand All @@ -576,7 +579,9 @@ export function calculateTvlWeightedApy(
const tvl = vault.current_nav ?? 0;
const apy = vault.one_month_cagr_net ?? vault.one_month_cagr;

if (tvl > 0 && apy != null && apy <= MAX_APY_THRESHOLD) {
if (options.maxTvlUsd != null && tvl > options.maxTvlUsd) continue;

if (Number.isFinite(tvl) && tvl > 0 && apy != null && apy <= MAX_APY_THRESHOLD) {
weightedSum += tvl * apy;
tvlSum += tvl;
}
Expand All @@ -588,8 +593,16 @@ export function calculateTvlWeightedApy(
/**
* Calculate total TVL for an array of vaults
*/
export function calculateTotalTvl(vaults: Pick<VaultInfo, 'current_nav'>[]): number {
return vaults.reduce((sum, v) => sum + (v.current_nav ?? 0), 0);
export function calculateTotalTvl(
vaults: Pick<VaultInfo, 'current_nav'>[],
options: { maxTvlUsd?: number } = {}
): number {
return vaults.reduce((sum, v) => {
const tvl = v.current_nav ?? 0;
if (!Number.isFinite(tvl)) return sum;
if (options.maxTvlUsd != null && tvl > options.maxTvlUsd) return sum;
return sum + tvl;
}, 0);
}

/**
Expand Down
24 changes: 16 additions & 8 deletions src/routes/trading-view/vaults/blacklisted/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ Blacklisted vault listing.
<script lang="ts">
import type { TopVaults } from '$lib/top-vaults/schemas';
import { fetchAllVaultData, hasVaultCache } from '$lib/top-vaults/client-cache';
import { getVaultCurrentTvlUsd, isBlacklisted } from '$lib/top-vaults/helpers';
import {
MAX_SUMMARY_TVL_USD,
calculateTotalTvl,
getVaultCurrentTvlUsd,
isBlacklisted
} from '$lib/top-vaults/helpers';
import { formatDollar } from '$lib/helpers/formatters';
import { page } from '$app/state';
import TopVaultsPage from '$lib/top-vaults/TopVaultsPage.svelte';
Expand All @@ -13,10 +18,6 @@ Blacklisted vault listing.
let topVaults = $state<TopVaults>();
let loading = $state(!hasVaultCache(page.data.generatedAt));

function isBlacklistedRankingVault(vault: TopVaults['vaults'][number]): boolean {
return isBlacklisted(vault) || vault.flags.includes('paused');
}

$effect(() => {
fetchAllVaultData(page.data.generatedAt)
.then((data) => (topVaults = data))
Expand All @@ -28,18 +29,23 @@ Blacklisted vault listing.
topVaults && {
...topVaults,
vaults: topVaults.vaults
.filter(isBlacklistedRankingVault)
.filter(isBlacklisted)
.toSorted((a, b) => (getVaultCurrentTvlUsd(b) ?? 0) - (getVaultCurrentTvlUsd(a) ?? 0))
}
);

const title = 'Blacklisted DeFi stablecoin vaults';
const description = 'Blacklisted DeFi stablecoin vaults, sorted by current TVL.';
let blacklistedTvl = $derived(
blacklistedTopVaults?.vaults.reduce((sum, vault) => sum + (getVaultCurrentTvlUsd(vault) ?? 0), 0) ?? 0
calculateTotalTvl(
blacklistedTopVaults?.vaults.map((vault) => ({ current_nav: getVaultCurrentTvlUsd(vault) })) ?? [],
{ maxTvlUsd: MAX_SUMMARY_TVL_USD }
)
);
let blacklistedCountText = $derived(blacklistedTopVaults ? String(blacklistedTopVaults.vaults.length) : '-');
let blacklistedTvlText = $derived(blacklistedTopVaults ? formatDollar(blacklistedTvl, 0) : '-');
let subtitle = $derived(
`Blacklisted ${blacklistedTopVaults?.vaults.length ?? 0} vaults and ${formatDollar(blacklistedTvl, 0)} TVL. Blacklisting reasons include illiquidity, depegging of the denomination currency and suspicious activities.`
`Blacklisted ${blacklistedCountText} vaults and ${blacklistedTvlText} TVL (some of this TVL is likely to be fake). Blacklisting reasons include illiquidity, depegging of the denominating fiat token, being a subvault of a composite, and suspicious activities.`
);
let pageUrl = $derived(new URL(page.url.pathname, page.url.origin).href);
</script>
Expand Down Expand Up @@ -72,6 +78,8 @@ Blacklisted vault listing.
{loading}
includeBlacklisted
includeBlacklistedInStats
maxSummaryTvlUsd={MAX_SUMMARY_TVL_USD}
disableBlacklistedStrikethrough
title="Blacklisted stablecoin vaults"
{subtitle}
showFilters
Expand Down
23 changes: 17 additions & 6 deletions tests/integration/trading-view/vaults/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,17 +135,28 @@ test.describe('vault index page', () => {

const rows = page.locator('tbody tr.targetable');
await expect(rows).toHaveCount(3);
await expect(rows.first()).toContainText('atvPTmax');
await expect(rows.nth(1)).toContainText('Paused blacklisted only vault');
await expect(rows.first()).toContainText('Abnormal TVL blacklisted vault');
await expect(rows.first()).not.toHaveClass(/blacklisted/);
await expect(rows.first().locator('td').first()).toHaveCSS('text-decoration-line', 'none');
await expect(rows.nth(1)).toContainText('atvPTmax');
await expect(rows.nth(2)).toContainText('Morpho flagged blacklisted vault');
await expect(
page.getByText(/Blacklisted 3 vaults and \$8M TVL \(some of this TVL is likely to be fake\)\./)
).toBeVisible();
await expect(
page.getByText(
'Blacklisting reasons include illiquidity, depegging of the denominating fiat token, being a subvault of a composite, and suspicious activities.'
)
).toBeVisible();
await expect(page.getByTestId('top-vaults-meta')).toContainText('3 vaults out of');
await expect(page.getByTestId('top-vaults-meta')).toContainText('TVL $8M');
await expect(page.getByTestId('top-vaults-meta')).toContainText('Avg. return 20.00%');
});

test('displays vault count in table meta', async ({ page }) => {
const meta = page.getByTestId('top-vaults-meta');
// Should show 255 vaults (those above TVL threshold)
await expect(meta).toContainText('255 vaults');
// Should show 254 vaults (those above TVL threshold)
await expect(meta).toContainText('254 vaults');
});

test('renders initial batch of 150 rows', async ({ page }) => {
Expand Down Expand Up @@ -186,9 +197,9 @@ test.describe('vault index page', () => {
await sentinel.scrollIntoViewIfNeeded();
await expect(rows).toHaveCount(250);

// scroll a third time - loads the final 5
// scroll a third time - loads the final 4
await sentinel.scrollIntoViewIfNeeded();
await expect(rows).toHaveCount(255);
await expect(rows).toHaveCount(254);

// all rows loaded - no more sentinel
await expect(sentinel).not.toBeVisible();
Expand Down
16 changes: 9 additions & 7 deletions tests/mocks/top-vaults/list.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,12 +354,14 @@ const morphoFlaggedBlacklistedVault = createTestVault('Morpho flagged blackliste
]
});

const pausedBlacklistedOnlyVault = createTestVault('Paused blacklisted only vault', {
chain: 'base',
protocol: 'Concrete',
current_nav: 50_000,
peak_nav: 75_000,
flags: ['paused']
const abnormalTvlBlacklistedVault = createTestVault('Abnormal TVL blacklisted vault', {
chain: 'arbitrum',
protocol: '<protocol not yet identified>',
current_nav: 2_000_000_000,
peak_nav: 2_000_000_000,
risk: 'Blacklisted',
flags: ['abnormal_tvl'],
notes: 'The TVL on this vault is abnormal'
});

const closedVaultPeriodResult = {
Expand Down Expand Up @@ -510,8 +512,8 @@ export default defineMock({
curators,
vaults: [
yamlStrategyVault,
abnormalTvlBlacklistedVault,
morphoFlaggedBlacklistedVault,
pausedBlacklistedOnlyVault,
depositDisabledVault,
redemptionDisabledVault,
depositAndRedemptionDisabledVault,
Expand Down
Loading