Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(beefy): add safety info #621

Merged
merged 3 commits into from
Oct 16, 2024
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
35 changes: 34 additions & 1 deletion locales/base.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
2 changes: 2 additions & 0 deletions src/apps/beefy/positions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
} from './api'
import { TFunction } from 'i18next'
import { networkIdToNativeAssetAddress } from '../../runtime/isNative'
import { getSafety } from './safety'

type BeefyPrices = Awaited<ReturnType<typeof getBeefyPrices>>
type BeefyApyBreakdown = Awaited<ReturnType<typeof getApyBreakdown>>
Expand Down Expand Up @@ -136,6 +137,7 @@ const beefyAppTokenDefinition = ({
apyBreakdown[vault.id],
vault,
),
safety: getSafety(vault, t),
},
availableShortcutIds: ['deposit', 'withdraw', 'swap-deposit'],
shortcutTriggerArgs: ({ tokensByTokenId }) => {
Expand Down
98 changes: 98 additions & 0 deletions src/apps/beefy/safety.test.ts
Original file line number Diff line number Diff line change
@@ -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',
},
],
})
})
})
78 changes: 78 additions & 0 deletions src/apps/beefy/safety.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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<string, string[]> = {}
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does clamp mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's the math.min if the score goes above 1, taken literally from beefy code

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')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice addition

}

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
// score represents the level of the risk, higher is worse
isPositive: score <= 0,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a bit confusing that isPositive is whether the score is <=0 but I think the naming makes sense for what it would be used for, not sure what a better alternative would be

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree, I added a clarifying comment

title: t(`beefyRisks.${title}`),
category: t(`beefyRisks.${category}`),
}
}),
}
}
Loading
Loading