Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .changeset/yield-plus-pr04-domain-utils.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@venusprotocol/evm": patch
---

add Yield+ domain models and pure utilities for position formatting, leverage calculations, and chart candle conversion
184 changes: 184 additions & 0 deletions apps/evm/src/__mocks__/models/yieldPlus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import type { YieldPlusPosition } from 'types';
import { convertTokensToMantissa } from 'utilities';
import { formatToYieldPlusPosition } from 'utilities/formatToYieldPlusPosition';
import fakeAddress, { altAddress } from './address';
import { poolData } from './pools';

type ApiYieldPlusPosition = {
pnl: {
realizedPnlShortAssetMantissa: string;
realizedPnlUsd: string;
unrealizedPnlShortAssetMantissa: string;
unrealizedPnlUsd: string;
unrealizedPnlPercentage: string;
entryRatio: string;
currentRatio: string;
closeEventsWithPnlData: unknown[];
totalShortOpenedMantissa: string;
totalLongReceivedMantissa: string;
};
positionAccountAddress: string;
accountAddress: string;
longVTokenAddress: string;
shortVTokenAddress: string;
chainId: string;
isActive: boolean;
cycleId: string;
dsaVTokenAddress: string;
effectiveLeverageRatio: string;
capitalUtilization: {
suppliedPrincipalMantissa: string;
capitalUtilizedMantissa: string;
withdrawableCapitalMantissa: string;
};
};

const pool = poolData[0];
const xvsAsset = pool.assets[0];
const usdcAsset = pool.assets[1];
const usdtAsset = pool.assets[2];
const busdAsset = pool.assets[3];

export const apiYieldPlusPositions: ApiYieldPlusPosition[] = [
{
pnl: {
realizedPnlShortAssetMantissa: '0',
realizedPnlUsd: '0',
unrealizedPnlShortAssetMantissa: '0',
unrealizedPnlUsd: '0',
unrealizedPnlPercentage: '0',
entryRatio: '1',
currentRatio: '1',
closeEventsWithPnlData: [],
totalShortOpenedMantissa: '0',
totalLongReceivedMantissa: '0',
},
positionAccountAddress: fakeAddress,
accountAddress: fakeAddress,
longVTokenAddress: usdtAsset.vToken.address,
shortVTokenAddress: busdAsset.vToken.address,
chainId: String(busdAsset.vToken.underlyingToken.chainId),
isActive: true,
cycleId: '1',
dsaVTokenAddress: xvsAsset.vToken.address,
effectiveLeverageRatio: '2',
capitalUtilization: {
suppliedPrincipalMantissa: convertTokensToMantissa({
value: xvsAsset.userSupplyBalanceTokens,
token: xvsAsset.vToken.underlyingToken,
}).toFixed(),
capitalUtilizedMantissa: '0',
withdrawableCapitalMantissa: '0',
},
},
{
positionAccountAddress: altAddress,
accountAddress: altAddress,
dsaVTokenAddress: usdcAsset.vToken.address,
longVTokenAddress: usdtAsset.vToken.address,
shortVTokenAddress: busdAsset.vToken.address,
chainId: String(busdAsset.vToken.underlyingToken.chainId),
isActive: true,
cycleId: '1',
effectiveLeverageRatio: '3',
pnl: {
realizedPnlShortAssetMantissa: '0',
realizedPnlUsd: '0',
unrealizedPnlShortAssetMantissa: '0',
unrealizedPnlUsd: '1.5',
unrealizedPnlPercentage: '1.2',
entryRatio: '1.1',
currentRatio: '1.1',
closeEventsWithPnlData: [],
totalShortOpenedMantissa: '0',
totalLongReceivedMantissa: '0',
},
capitalUtilization: {
suppliedPrincipalMantissa: convertTokensToMantissa({
value: usdcAsset.userSupplyBalanceTokens,
token: usdcAsset.vToken.underlyingToken,
}).toFixed(),
capitalUtilizedMantissa: '0',
withdrawableCapitalMantissa: '0',
},
},
{
positionAccountAddress: altAddress,
accountAddress: altAddress,
dsaVTokenAddress: usdcAsset.vToken.address,
longVTokenAddress: usdcAsset.vToken.address,
shortVTokenAddress: usdtAsset.vToken.address,
chainId: String(usdtAsset.vToken.underlyingToken.chainId),
isActive: true,
cycleId: '1',
effectiveLeverageRatio: '1.5',
pnl: {
realizedPnlShortAssetMantissa: '0',
realizedPnlUsd: '0',
unrealizedPnlShortAssetMantissa: '0',
unrealizedPnlUsd: '-0.5',
unrealizedPnlPercentage: '-0.4',
entryRatio: '0.95',
currentRatio: '0.95',
closeEventsWithPnlData: [],
totalShortOpenedMantissa: '0',
totalLongReceivedMantissa: '0',
},
capitalUtilization: {
suppliedPrincipalMantissa: convertTokensToMantissa({
value: usdcAsset.userSupplyBalanceTokens,
token: usdcAsset.vToken.underlyingToken,
}).toFixed(),
capitalUtilizedMantissa: '0',
withdrawableCapitalMantissa: '0',
},
},
];

export const yieldPlusPositions: YieldPlusPosition[] = [
formatToYieldPlusPosition({
pool,
chainId: busdAsset.vToken.underlyingToken.chainId,
positionAccountAddress: fakeAddress,
dsaVTokenAddress: xvsAsset.vToken.address,
dsaBalanceMantissa: convertTokensToMantissa({
value: xvsAsset.userSupplyBalanceTokens,
token: xvsAsset.vToken.underlyingToken,
}),
longVTokenAddress: usdtAsset.vToken.address,
shortVTokenAddress: busdAsset.vToken.address,
leverageFactor: 2,
unrealizedPnlCents: 0,
unrealizedPnlPercentage: 0,
})!,
formatToYieldPlusPosition({
pool,
chainId: busdAsset.vToken.underlyingToken.chainId,
positionAccountAddress: altAddress,
dsaVTokenAddress: usdcAsset.vToken.address,
dsaBalanceMantissa: convertTokensToMantissa({
value: usdcAsset.userSupplyBalanceTokens,
token: usdcAsset.vToken.underlyingToken,
}),
longVTokenAddress: usdtAsset.vToken.address,
shortVTokenAddress: busdAsset.vToken.address,
leverageFactor: 3,
unrealizedPnlCents: 150,
unrealizedPnlPercentage: 1.2,
})!,
formatToYieldPlusPosition({
pool,
chainId: usdtAsset.vToken.underlyingToken.chainId,
positionAccountAddress: altAddress,
dsaVTokenAddress: usdcAsset.vToken.address,
dsaBalanceMantissa: convertTokensToMantissa({
value: usdcAsset.userSupplyBalanceTokens,
token: usdcAsset.vToken.underlyingToken,
}),
longVTokenAddress: usdcAsset.vToken.address,
shortVTokenAddress: usdtAsset.vToken.address,
leverageFactor: 1.5,
unrealizedPnlCents: -50,
unrealizedPnlPercentage: -0.4,
})!,
];
5 changes: 5 additions & 0 deletions apps/evm/src/libs/translations/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2189,5 +2189,10 @@
"submitButton": "Withdraw"
},
"withdrawTabTitle": "Withdraw"
},
"yieldPlus": {
"operationForm": {
"invalidLiquidationPrice": "N/A"
}
}
}
5 changes: 5 additions & 0 deletions apps/evm/src/libs/translations/translations/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -2188,5 +2188,10 @@
"submitButton": "引き出す"
},
"withdrawTabTitle": "引き出す"
},
"yieldPlus": {
"operationForm": {
"invalidLiquidationPrice": "該当なし"
}
}
}
5 changes: 5 additions & 0 deletions apps/evm/src/libs/translations/translations/th.json
Original file line number Diff line number Diff line change
Expand Up @@ -2188,5 +2188,10 @@
"submitButton": "ถอน"
},
"withdrawTabTitle": "ถอน"
},
"yieldPlus": {
"operationForm": {
"invalidLiquidationPrice": "ไม่มี"
}
}
}
5 changes: 5 additions & 0 deletions apps/evm/src/libs/translations/translations/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -2189,5 +2189,10 @@
"submitButton": "Çek"
},
"withdrawTabTitle": "Çek"
},
"yieldPlus": {
"operationForm": {
"invalidLiquidationPrice": "Yok"
}
}
}
5 changes: 5 additions & 0 deletions apps/evm/src/libs/translations/translations/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2188,5 +2188,10 @@
"submitButton": "Rút"
},
"withdrawTabTitle": "Rút"
},
"yieldPlus": {
"operationForm": {
"invalidLiquidationPrice": "Không áp dụng"
}
}
}
5 changes: 5 additions & 0 deletions apps/evm/src/libs/translations/translations/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -2188,5 +2188,10 @@
"submitButton": "取款"
},
"withdrawTabTitle": "取款"
},
"yieldPlus": {
"operationForm": {
"invalidLiquidationPrice": "不适用"
}
}
}
5 changes: 5 additions & 0 deletions apps/evm/src/libs/translations/translations/zh-Hant.json
Original file line number Diff line number Diff line change
Expand Up @@ -2188,5 +2188,10 @@
"submitButton": "提取"
},
"withdrawTabTitle": "提取"
},
"yieldPlus": {
"operationForm": {
"invalidLiquidationPrice": "不適用"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { calculateMaxLeverageFactor } from '..';

describe('calculateMaxLeverageFactor', () => {
it('returns the leverage factor derived from the collateral factors and close tolerance', () => {
const result = calculateMaxLeverageFactor({
dsaTokenCollateralFactor: 0.5,
longTokenCollateralFactor: 0.8,
proportionalCloseTolerancePercentage: 2,
});

expect(result).toMatchInlineSnapshot('2.31');
});

it('rounds down to two decimal places', () => {
const result = calculateMaxLeverageFactor({
dsaTokenCollateralFactor: 0.1,
longTokenCollateralFactor: 0.7,
proportionalCloseTolerancePercentage: 0,
});

expect(result).toMatchInlineSnapshot('0.33');
});

it('reduces the maximum leverage when proportional close tolerance increases', () => {
const lowToleranceResult = calculateMaxLeverageFactor({
dsaTokenCollateralFactor: 0.5,
longTokenCollateralFactor: 0.8,
proportionalCloseTolerancePercentage: 2,
});
const highToleranceResult = calculateMaxLeverageFactor({
dsaTokenCollateralFactor: 0.5,
longTokenCollateralFactor: 0.8,
proportionalCloseTolerancePercentage: 10,
});

expect(lowToleranceResult).toMatchInlineSnapshot('2.31');
expect(highToleranceResult).toMatchInlineSnapshot('1.78');
});
});
19 changes: 19 additions & 0 deletions apps/evm/src/pages/YieldPlus/calculateMaxLeverageFactor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import BigNumber from 'bignumber.js';

export interface CalculateMaxLeverageFactorInput {
dsaTokenCollateralFactor: number;
longTokenCollateralFactor: number;
proportionalCloseTolerancePercentage: number;
}

export const calculateMaxLeverageFactor = ({
dsaTokenCollateralFactor,
longTokenCollateralFactor,
proportionalCloseTolerancePercentage,
}: CalculateMaxLeverageFactorInput) =>
new BigNumber(
dsaTokenCollateralFactor /
(1 - longTokenCollateralFactor * (1 - proportionalCloseTolerancePercentage / 100)),
)
Comment on lines +14 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Native floating-point arithmetic before BigNumber wrapping

The denominator is computed entirely in native JS floating-point before being passed to BigNumber. This discards BigNumber's precision guarantees — for deeply fractional inputs, the intermediate result can already be slightly off before rounding. Additionally, if longTokenCollateralFactor === 1 and proportionalCloseTolerancePercentage === 0, the denominator is exactly 0, giving Infinity (which propagates through .dp(2).toNumber() as Infinity). Callers would need to guard against this.

Prefer computing entirely in BigNumber:

Suggested change
new BigNumber(
dsaTokenCollateralFactor /
(1 - longTokenCollateralFactor * (1 - proportionalCloseTolerancePercentage / 100)),
)
const denominator = new BigNumber(1).minus(
new BigNumber(longTokenCollateralFactor).multipliedBy(
new BigNumber(1).minus(new BigNumber(proportionalCloseTolerancePercentage).dividedBy(100)),
),
);
if (denominator.isZero()) {
return Infinity;
}
return new BigNumber(dsaTokenCollateralFactor)
.dividedBy(denominator)
.dp(2, BigNumber.ROUND_DOWN)
.toNumber();

.dp(2, BigNumber.ROUND_DOWN)
.toNumber();
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { TFunction } from 'i18next';

import { type FormatTokensToReadableValueInput, formatTokensToReadableValue } from 'utilities';

export interface FormatLiquidationPriceTokensToReadableValueInput
extends FormatTokensToReadableValueInput {
t: TFunction<'translation', undefined>;
}

export const formatLiquidationPriceTokensToReadableValue = (
_input: FormatLiquidationPriceTokensToReadableValueInput,
) => {
const { t, ...input } = _input;

if (input.value?.isLessThanOrEqualTo(0)) {
return t('yieldPlus.operationForm.invalidLiquidationPrice');
}

return formatTokensToReadableValue({ ...input, addSymbol: false });
};
22 changes: 22 additions & 0 deletions apps/evm/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,28 @@ export interface ImportableAaveSupplyPosition extends ImportableSupplyPositionBa

export type ImportableSupplyPosition = ImportableAaveSupplyPosition;

export interface YieldPlusPosition {
chainId: ChainId;
positionAccountAddress: Address;
longAsset: Asset;
longBalanceTokens: BigNumber;
longBalanceCents: number;
shortAsset: Asset;
shortBalanceTokens: BigNumber;
shortBalanceCents: number;
dsaAsset: Asset;
dsaBalanceTokens: BigNumber;
dsaBalanceCents: number;
netValueCents: number;
netApyPercentage: number;
unrealizedPnlCents: number;
unrealizedPnlPercentage: number;
liquidationPriceTokens: BigNumber;
entryPriceTokens: BigNumber;
leverageFactor: number;
pool: Pool;
}

export type CommonTxFormErrorCode =
| 'SUPPLY_CAP_ALREADY_REACHED'
| 'BORROW_CAP_ALREADY_REACHED'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { convertPercentageToBps } from '..';

describe('convertPercentageToBps', () => {
it('converts whole percentages to bps', () => {
expect(convertPercentageToBps({ percentage: 50 })).toBe(5000n);
});

it('converts decimal percentages to bps', () => {
expect(convertPercentageToBps({ percentage: 12.34 })).toBe(1234n);
});
});
4 changes: 4 additions & 0 deletions apps/evm/src/utilities/convertPercentageToBps/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import BigNumber from 'bignumber.js';

export const convertPercentageToBps = ({ percentage }: { percentage: number }) =>
BigInt(new BigNumber(percentage).multipliedBy(100).toFixed(0));
Loading
Loading