From 6ecbe6f6dc26af87d8b8cd9d4f3aa583e4d3e34e Mon Sep 17 00:00:00 2001 From: Satish Ravi Date: Tue, 15 Oct 2024 23:10:45 -0700 Subject: [PATCH 1/2] feat(beefy): add safety info --- locales/base.json | 35 ++++++- src/apps/beefy/positions.ts | 2 + src/apps/beefy/safety.test.ts | 98 +++++++++++++++++ src/apps/beefy/safety.ts | 77 ++++++++++++++ src/apps/beefy/safetyConfig.ts | 185 +++++++++++++++++++++++++++++++++ src/types/positions.ts | 12 +++ 6 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 src/apps/beefy/safety.test.ts create mode 100644 src/apps/beefy/safety.ts create mode 100644 src/apps/beefy/safetyConfig.ts diff --git a/locales/base.json b/locales/base.json index a2bc8096..2e7cbe63 100644 --- a/locales/base.json +++ b/locales/base.json @@ -3,5 +3,38 @@ "earningsApy": "Earnings APY", "earningsApr": "Earnings APR" }, - "earningItems": { "rewards": "Rewards", "earnings": "Earnings" } + "earningItems": { "rewards": "Rewards", "earnings": "Earnings" }, + "beefyRisks": { + "Categry-Beefy": "Beefy", + "Categry-Asset": "Asset", + "Categry-Platform": "Platform", + "Complexity-Low-Titl": "Low-complexity strategy", + "Complexity-Mid-Titl": "Medium-complexity strategy", + "Complexity-Hi-Titl": "High-complexity strategy", + "Testd-Battle-Titl": "Strategy is battle tested", + "Testd-New-Titl": "Strategy is new", + "Testd-Experimtl-Titl": "Strategy is experimental", + "IL-None-Titl": "Very low or zero expected IL", + "IL-Low-Titl": "Low expected IL", + "IL-High-Titl": "High expected IL", + "IL-AlgoStable-Titl": "Algorithmic stable, polarized IL risk", + "OverCollatAlgoStable-Titl": "Overcollateralized algorithmic stablecoin", + "PartialCollatAlgoStable-Titl": "Partially-collateralized algorithmic stablecoin", + "Liquidt-High-Titl": "High asset liquidity", + "Liquidt-Low-Titl": "Low asset liquidity", + "MktCap-Large-Titl": "High market-capitalization, lower volatility asset", + "MktCap-Mid-Titl": "Medium market-capitalization, average volatility asset", + "MktCap-Small-Titl": "Small market-capitalization, high volatility asset", + "MktCap-Micro-Titl": "Micro market-capitalization, extreme volatility asset", + "Concentrated-Titl": "Asset supply concentrated", + "Platfrm-Establshd-Titl": "Platform with known track record", + "Platfrm-New-Titl": "Platform with little or no track record", + "Platfrm-AuditNo-Titl": "No established audit", + "Platfrm-Audit-Titl": "Platform audited by trusted reviewer", + "Platfrm-Verified-Titl": "Project contracts are verified", + "Platfrm-VerifiedNo-Titl": "Project contracts non-public (unverified)", + "Platfrm-Timelock-Titl": "Dangerous functions timelocked", + "Platfrm-TimelockShort-Titl": "Dangerous functions timelocked with short delay", + "Platfrm-TimelockNo-Titl": "Dangerous function not timelocked" + } } diff --git a/src/apps/beefy/positions.ts b/src/apps/beefy/positions.ts index 04ad7b24..2687611f 100644 --- a/src/apps/beefy/positions.ts +++ b/src/apps/beefy/positions.ts @@ -28,6 +28,7 @@ import { } from './api' import { TFunction } from 'i18next' import { networkIdToNativeAssetAddress } from '../../runtime/isNative' +import { getSafety } from './safety' type BeefyPrices = Awaited> type BeefyApys = Awaited> @@ -132,6 +133,7 @@ const beefyAppTokenDefinition = ({ manageUrl: `${BEEFY_VAULT_BASE_URL}${vault.id}`, contractCreatedAt: new Date(vault.createdAt * 1000).toISOString(), claimType: ClaimType.Rewards, + safety: getSafety(vault, t), }, availableShortcutIds: ['deposit', 'withdraw', 'swap-deposit'], shortcutTriggerArgs: ({ tokensByTokenId }) => { diff --git a/src/apps/beefy/safety.test.ts b/src/apps/beefy/safety.test.ts new file mode 100644 index 00000000..3ce80501 --- /dev/null +++ b/src/apps/beefy/safety.test.ts @@ -0,0 +1,98 @@ +import { TFunction } from 'i18next' +import { BaseBeefyVault } from './api' +import { getSafety } from './safety' + +const mockT = ((x: string) => x) as TFunction + +describe('safety', () => { + it('returns safety info for known risks', () => { + const safety = getSafety( + { + risks: [ + 'COMPLEXITY_LOW', + 'BATTLE_TESTED', + 'IL_NONE', + 'MCAP_LARGE', + 'AUDIT', + 'CONTRACTS_VERIFIED', + ], + id: 'mock-vault', + } as BaseBeefyVault, + mockT, + ) + + expect(safety).toEqual({ + level: 'high', + risks: [ + { + category: 'beefyRisks.Categry-Beefy', + isPositive: true, + title: 'beefyRisks.Complexity-Low-Titl', + }, + { + category: 'beefyRisks.Categry-Beefy', + isPositive: true, + title: 'beefyRisks.Testd-Battle-Titl', + }, + { + category: 'beefyRisks.Categry-Asset', + isPositive: true, + title: 'beefyRisks.IL-None-Titl', + }, + { + category: 'beefyRisks.Categry-Asset', + isPositive: true, + title: 'beefyRisks.MktCap-Large-Titl', + }, + { + category: 'beefyRisks.Categry-Platform', + isPositive: true, + title: 'beefyRisks.Platfrm-Audit-Titl', + }, + { + category: 'beefyRisks.Categry-Platform', + isPositive: true, + title: 'beefyRisks.Platfrm-Verified-Titl', + }, + ], + }) + }) + + it('returns undefined if no known risks', () => { + const safety = getSafety( + { + risks: ['foo', 'bar'] as string[], + id: 'mock-vault', + } as BaseBeefyVault, + mockT, + ) + + expect(safety).toBeUndefined() + }) + + it('excludes unknown risks if there are some unknown risks', () => { + const safety = getSafety( + { + risks: ['foo', 'EXPERIMENTAL_STRAT', 'MCAP_MICRO'] as string[], + id: 'mock-vault', + } as BaseBeefyVault, + mockT, + ) + + expect(safety).toEqual({ + level: 'medium', + risks: [ + { + category: 'beefyRisks.Categry-Beefy', + isPositive: false, + title: 'beefyRisks.Testd-Experimtl-Titl', + }, + { + category: 'beefyRisks.Categry-Asset', + isPositive: false, + title: 'beefyRisks.MktCap-Micro-Titl', + }, + ], + }) + }) +}) diff --git a/src/apps/beefy/safety.ts b/src/apps/beefy/safety.ts new file mode 100644 index 00000000..0f77ff89 --- /dev/null +++ b/src/apps/beefy/safety.ts @@ -0,0 +1,77 @@ +import { TFunction } from 'i18next' +import { Safety } from '../../types/positions' +import { MAX_SCORE, RISKS, CATEGORIES } from './safetyConfig' +import { logger } from '../../log' +import { BaseBeefyVault } from './api' + +// From https://github.com/beefyfinance/beefy-v2/blob/3690e105c4bb98afcf06f9c3e385d13cc23af5cd/src/helpers/safetyScore.tsx +const calcRisk = (risks: string[]) => { + const categories: Record = {} + for (const c of Object.keys(CATEGORIES)) { + categories[c] = [] + } + + // reverse lookup + risks.forEach((risk) => { + // should never happen with check below, but leaving as is from beefy codebase + if (!(risk in RISKS)) { + return + } + + // should never happen with type safety, but leaving as is from beefy codebase + const cat = RISKS[risk].category + if (!(cat in CATEGORIES)) { + return + } + + categories[cat].push(risk) + }) + + // reduce & clamp + let score = 0 + for (const [category, weight] of Object.entries(CATEGORIES)) { + score += + weight * + Math.min( + 1, + categories[category].reduce( + (acc: number, risk: string) => acc + RISKS[risk].score, + 0, + ), + ) + } + + return score +} + +const safetyScore = (risks: string[]) => { + const score = MAX_SCORE * (1 - calcRisk(risks)) + // from https://github.com/beefyfinance/beefy-v2/blob/3690e105c4bb98afcf06f9c3e385d13cc23af5cd/src/components/SafetyScore/SafetyScore.tsx#L27-L29 + return score > 7.5 ? 'high' : score >= 6.4 ? 'medium' : 'low' +} + +export function getSafety( + vault: BaseBeefyVault, + t: TFunction, +): Safety | undefined { + const { risks } = vault + const knownRisks = risks.filter((risk) => !!RISKS[risk]) + + if (knownRisks.length !== risks.length) { + logger.warn({ vault }, 'Beefy vault has unknown risks') + } + + if (!knownRisks.length) return + return { + level: safetyScore(knownRisks), + risks: knownRisks.map((risk) => { + const { category, title, score } = RISKS[risk] + return { + // from https://github.com/beefyfinance/beefy-v2/blob/3690e105c4bb98afcf06f9c3e385d13cc23af5cd/src/features/vault/components/SafetyCard/SafetyCard.tsx#L39 + isPositive: score <= 0, + title: t(`beefyRisks.${title}`), + category: t(`beefyRisks.${category}`), + } + }), + } +} diff --git a/src/apps/beefy/safetyConfig.ts b/src/apps/beefy/safetyConfig.ts new file mode 100644 index 00000000..1ad81df5 --- /dev/null +++ b/src/apps/beefy/safetyConfig.ts @@ -0,0 +1,185 @@ +// from https://github.com/beefyfinance/beefy-v2/blob/3690e105c4bb98afcf06f9c3e385d13cc23af5cd/src/config/risk.tsx +export const MAX_SCORE = 10 + +type Risk = { + category: keyof typeof CATEGORIES + score: number + title: string +} + +// removed explanation, condition and risks without category / score from the beefy codebase +export const RISKS: Record = { + COMPLEXITY_LOW: { + category: 'Categry-Beefy', + score: 0, + title: 'Complexity-Low-Titl', + }, + + COMPLEXITY_MID: { + category: 'Categry-Beefy', + score: 0.3, + title: 'Complexity-Mid-Titl', + }, + + COMPLEXITY_HIGH: { + category: 'Categry-Beefy', + score: 0.5, + title: 'Complexity-Hi-Titl', + }, + + BATTLE_TESTED: { + category: 'Categry-Beefy', + score: 0, + title: 'Testd-Battle-Titl', + }, + + NEW_STRAT: { + category: 'Categry-Beefy', + score: 0.3, + title: 'Testd-New-Titl', + }, + + EXPERIMENTAL_STRAT: { + category: 'Categry-Beefy', + score: 0.7, + title: 'Testd-Experimtl-Titl', + }, + + IL_NONE: { + category: 'Categry-Asset', + score: 0, + title: 'IL-None-Titl', + }, + + IL_LOW: { + category: 'Categry-Asset', + score: 0.2, + title: 'IL-Low-Titl', + }, + + IL_HIGH: { + category: 'Categry-Asset', + score: 0.5, + title: 'IL-High-Titl', + }, + + ALGO_STABLE: { + category: 'Categry-Asset', + score: 0.9, + title: 'IL-AlgoStable-Titl', + }, + + PARTIAL_COLLAT_ALGO_STABLECOIN: { + category: 'Categry-Asset', + score: 0.21, + title: 'PartialCollatAlgoStable-Titl', + }, + + OVER_COLLAT_ALGO_STABLECOIN: { + category: 'Categry-Asset', + score: 0.15, + title: 'OverCollatAlgoStable-Titl', + }, + + LIQ_HIGH: { + category: 'Categry-Asset', + score: 0, + title: 'Liquidt-High-Titl', + }, + + LIQ_LOW: { + category: 'Categry-Asset', + score: 0.3, + title: 'Liquidt-Low-Titl', + }, + + MCAP_LARGE: { + category: 'Categry-Asset', + score: 0, + title: 'MktCap-Large-Titl', + }, + + MCAP_MEDIUM: { + category: 'Categry-Asset', + score: 0.1, + title: 'MktCap-Mid-Titl', + }, + + MCAP_SMALL: { + category: 'Categry-Asset', + score: 0.3, + title: 'MktCap-Small-Titl', + }, + + MCAP_MICRO: { + category: 'Categry-Asset', + score: 0.5, + title: 'MktCap-Micro-Titl', + }, + + SUPPLY_CENTRALIZED: { + category: 'Categry-Asset', + score: 1, + title: 'Concentrated-Titl', + }, + + PLATFORM_ESTABLISHED: { + category: 'Categry-Platform', + score: 0, + title: 'Platfrm-Establshd-Titl', + }, + + PLATFORM_NEW: { + category: 'Categry-Platform', + score: 0.5, + title: 'Platfrm-New-Titl', + }, + + NO_AUDIT: { + category: 'Categry-Platform', + score: 0.3, + title: 'Platfrm-AuditNo-Titl', + }, + + AUDIT: { + category: 'Categry-Platform', + score: 0, + title: 'Platfrm-Audit-Titl', + }, + + CONTRACTS_VERIFIED: { + category: 'Categry-Platform', + score: 0, + title: 'Platfrm-Verified-Titl', + }, + + CONTRACTS_UNVERIFIED: { + category: 'Categry-Platform', + score: 1, + title: 'Platfrm-VerifiedNo-Titl', + }, + + ADMIN_WITH_TIMELOCK: { + category: 'Categry-Platform', + score: 0, + title: 'Platfrm-Timelock-Titl', + }, + + ADMIN_WITH_SHORT_TIMELOCK: { + category: 'Categry-Platform', + score: 0.5, + title: 'Platfrm-TimelockShort-Titl', + }, + + ADMIN_WITHOUT_TIMELOCK: { + category: 'Categry-Platform', + score: 1, + title: 'Platfrm-TimelockNo-Titl', + }, +} + +export const CATEGORIES = { + 'Categry-Beefy': 0.2, + 'Categry-Asset': 0.3, + 'Categry-Platform': 0.5, +} diff --git a/src/types/positions.ts b/src/types/positions.ts index 9af69e41..0516879a 100644 --- a/src/types/positions.ts +++ b/src/types/positions.ts @@ -87,6 +87,17 @@ export enum ClaimType { Rewards = 'rewards', } +export interface SafetyRisk { + isPositive: boolean + title: string + category: string +} + +export interface Safety { + level: 'low' | 'medium' | 'high' + risks: SafetyRisk[] +} + export interface EarnDataProps { contractCreatedAt?: string // ISO string manageUrl?: string @@ -100,6 +111,7 @@ export interface EarnDataProps { rewardsPositionIds?: string[] claimType?: ClaimType withdrawalIncludesClaim?: boolean + safety?: Safety // We'll add more fields here as needed } From 38610a87f122a4c69a5135269472d3925f9ec994 Mon Sep 17 00:00:00 2001 From: Satish Ravi Date: Wed, 16 Oct 2024 13:10:21 -0700 Subject: [PATCH 2/2] clarify --- src/apps/beefy/safety.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apps/beefy/safety.ts b/src/apps/beefy/safety.ts index 0f77ff89..6d78f94c 100644 --- a/src/apps/beefy/safety.ts +++ b/src/apps/beefy/safety.ts @@ -68,6 +68,7 @@ export function getSafety( const { category, title, score } = RISKS[risk] return { // from https://github.com/beefyfinance/beefy-v2/blob/3690e105c4bb98afcf06f9c3e385d13cc23af5cd/src/features/vault/components/SafetyCard/SafetyCard.tsx#L39 + // score represents the level of the risk, higher is worse isPositive: score <= 0, title: t(`beefyRisks.${title}`), category: t(`beefyRisks.${category}`),