Skip to content

Commit

Permalink
Add custom tag hex and liquidation auction info to advanced page (#822)
Browse files Browse the repository at this point in the history
  • Loading branch information
haydenshively authored Feb 26, 2024
1 parent ed1609d commit 529ec04
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 70 deletions.
145 changes: 82 additions & 63 deletions earn/src/components/advanced/BorrowMetrics.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { useMemo } from 'react';
import { ReactNode, useContext, useEffect, useMemo, useState } from 'react';

import { formatDistanceToNowStrict } from 'date-fns';
import Tooltip from 'shared/lib/components/common/Tooltip';
import { Display, Text } from 'shared/lib/components/common/Typography';
import { MANAGER_NAME_MAP } from 'shared/lib/data/constants/ChainSpecific';
import { GREY_700 } from 'shared/lib/data/constants/Colors';
import useSafeState from 'shared/lib/data/hooks/UseSafeState';
import { getEtherscanUrlForChain } from 'shared/lib/util/Chains';
import { formatTokenAmount } from 'shared/lib/util/Numbers';
import styled from 'styled-components';
import { Address } from 'wagmi';

import { computeLiquidationThresholds, getAssets, sqrtRatioToPrice, sqrtRatioToTick } from '../../data/BalanceSheet';
import { ChainContext } from '../../App';
import { auctionCurve, sqrtRatioToTick } from '../../data/BalanceSheet';
import { BorrowerNftBorrower } from '../../data/BorrowerNft';
import { RESPONSIVE_BREAKPOINT_MD, RESPONSIVE_BREAKPOINT_SM } from '../../data/constants/Breakpoints';
import { MarginAccount } from '../../data/MarginAccount';

const BORROW_TITLE_TEXT_COLOR = 'rgba(130, 160, 182, 1)';
const MAX_HEALTH = 10;
Expand Down Expand Up @@ -116,14 +122,14 @@ function MetricCard(props: { label: string; value: string }) {
);
}

function HorizontalMetricCard(props: { label: string; value: string }) {
const { label, value } = props;
function HorizontalMetricCard(props: { label: string; value?: string; children?: ReactNode }) {
const { label, value, children } = props;
return (
<HorizontalMetricCardContainer>
<Text size='M' color={BORROW_TITLE_TEXT_COLOR}>
{label}
</Text>
<Display size='S'>{value}</Display>
{children === undefined ? <Display size='S'>{value}</Display> : children}
</HorizontalMetricCardContainer>
);
}
Expand Down Expand Up @@ -157,7 +163,7 @@ function HealthMetricCard(props: { health: number }) {
}

export type BorrowMetricsProps = {
marginAccount?: MarginAccount;
marginAccount?: BorrowerNftBorrower;
dailyInterest0: number;
dailyInterest1: number;
userHasNoMarginAccounts: boolean;
Expand All @@ -166,62 +172,30 @@ export type BorrowMetricsProps = {
export function BorrowMetrics(props: BorrowMetricsProps) {
const { marginAccount, dailyInterest0, dailyInterest1, userHasNoMarginAccounts } = props;

const { activeChain } = useContext(ChainContext);

const [, setCurrentTime] = useState(Date.now());
const [mostRecentModifyTime, setMostRecentModifyTime] = useSafeState<Date | null>(null);

const [token0Collateral, token1Collateral] = useMemo(
() => marginAccount?.assets.amountsAt(sqrtRatioToTick(marginAccount.sqrtPriceX96)) ?? [0, 0],
[marginAccount]
);

const maxSafeCollateralFall = useMemo(() => {
if (!marginAccount) return null;

const { lowerSqrtRatio, upperSqrtRatio, minSqrtRatio, maxSqrtRatio } = computeLiquidationThresholds(
marginAccount.assets,
marginAccount.liabilities,
marginAccount.assets.uniswapPositions,
marginAccount.sqrtPriceX96,
marginAccount.iv,
marginAccount.nSigma,
marginAccount.token0.decimals,
marginAccount.token1.decimals
);

if (lowerSqrtRatio.eq(minSqrtRatio) && upperSqrtRatio.eq(maxSqrtRatio)) return Number.POSITIVE_INFINITY;

const [current, lower, upper] = [marginAccount.sqrtPriceX96, lowerSqrtRatio, upperSqrtRatio].map((sp) =>
sqrtRatioToPrice(sp, marginAccount.token0.decimals, marginAccount.token1.decimals)
);

const assets = getAssets(marginAccount.assets, lowerSqrtRatio, upperSqrtRatio);

// Compute the value of all assets (collateral) at 3 different prices (current, lower, and upper)
// Denominated in units of token1
let assetValueCurrent = token0Collateral * current + token1Collateral;
let assetValueAtLower = assets.amount0AtA * lower + assets.amount1AtA;
let assetValueAtUpper = assets.amount0AtB * upper + assets.amount1AtB;

// If there are no assets, further results would be spurious, so return null
if (assetValueCurrent < Number.EPSILON) return null;

// Compute how much the collateral can drop in value while remaining solvent
const percentChange1A = Math.abs(assetValueCurrent - assetValueAtLower) / assetValueCurrent;
const percentChange1B = Math.abs(assetValueCurrent - assetValueAtUpper) / assetValueCurrent;
const percentChange1 = Math.min(percentChange1A, percentChange1B);

// Now change to units of token0
assetValueCurrent /= current;
assetValueAtLower /= lower;
assetValueAtUpper /= upper;

// Again compute how much the collateral can drop in value while remaining solvent,
// but this time percentages are based on units of token0
const percentChange0A = Math.abs(assetValueCurrent - assetValueAtLower) / assetValueCurrent;
const percentChange0B = Math.abs(assetValueCurrent - assetValueAtUpper) / assetValueCurrent;
const percentChange0 = Math.min(percentChange0A, percentChange0B);

// Since we don't know whether the user is thinking in terms of "X per Y" or "Y per X",
// we return the minimum. Error on the side of being too conservative.
return Math.min(percentChange0, percentChange1);
}, [marginAccount, token0Collateral, token1Collateral]);
useEffect(() => {
(async () => {
setMostRecentModifyTime(null);
if (!marginAccount?.mostRecentModify) return;
const block = await marginAccount.mostRecentModify.getBlock();
setMostRecentModifyTime(new Date(block.timestamp * 1000));
})();
}, [marginAccount, setMostRecentModifyTime]);

useEffect(() => {
const interval = setInterval(() => setCurrentTime(Date.now()), 200);
if (!marginAccount?.warningTime) clearInterval(interval);
return () => clearInterval(interval);
}, [marginAccount?.warningTime]);

if (!marginAccount)
return (
Expand All @@ -236,14 +210,39 @@ export function BorrowMetrics(props: BorrowMetricsProps) {
<MetricCardPlaceholder height={56} $animate={!userHasNoMarginAccounts} />
<MetricCardPlaceholder height={56} $animate={!userHasNoMarginAccounts} />
<MetricCardPlaceholder height={56} $animate={!userHasNoMarginAccounts} />
<MetricCardPlaceholder height={56} $animate={!userHasNoMarginAccounts} />
</MetricsGridLower>
</MetricsGrid>
);

let liquidationDistanceText = '-';
if (maxSafeCollateralFall !== null) {
if (maxSafeCollateralFall === Number.POSITIVE_INFINITY) liquidationDistanceText = '∞';
else liquidationDistanceText = `${(maxSafeCollateralFall * 100).toPrecision(2)}% drop in collateral value`;
const etherscanUrl = getEtherscanUrlForChain(activeChain);

const mostRecentManager = marginAccount.mostRecentModify
? (marginAccount.mostRecentModify.args!['manager'] as Address)
: '0x';
const mostRecentManagerName = Object.hasOwn(MANAGER_NAME_MAP, mostRecentManager)
? MANAGER_NAME_MAP[mostRecentManager]
: undefined;
const mostRecentManagerUrl = `${etherscanUrl}/address/${mostRecentManager}`;

const mostRecentModifyHash = marginAccount.mostRecentModify?.transactionHash;
const mostRecentModifyUrl = `${etherscanUrl}/tx/${mostRecentModifyHash}`;
const mostRecentModifyTimeStr = mostRecentModifyTime
? formatDistanceToNowStrict(mostRecentModifyTime, {
addSuffix: true,
roundingMethod: 'round',
})
: '';

let liquidationAuctionStr = 'Not started';
if (marginAccount.warningTime > 0) {
const auctionStartTime = marginAccount.warningTime + 5 * 60;
const currentTime = Date.now() / 1000;
if (currentTime < auctionStartTime) {
liquidationAuctionStr = `Begins in ${(auctionStartTime - currentTime).toFixed(1)} seconds`;
} else {
liquidationAuctionStr = `${(auctionCurve(currentTime - auctionStartTime) * 100 - 100).toFixed(2)}% incentive`;
}
}

return (
Expand All @@ -268,14 +267,34 @@ export function BorrowMetrics(props: BorrowMetricsProps) {
</MetricsGridUpper>
<MetricsGridLower>
<HealthMetricCard health={marginAccount.health || 0} />
<HorizontalMetricCard label='Liquidation Distance' value={liquidationDistanceText} />
<HorizontalMetricCard
label='Daily Interest Owed'
value={`${formatTokenAmount(dailyInterest0, 2)} ${marginAccount.token0.symbol},  ${formatTokenAmount(
dailyInterest1,
2
)} ${marginAccount.token1.symbol}`}
/>
{mostRecentModifyHash && (
<HorizontalMetricCard label='Last Modified'>
<Text size='M'>
<a className='underline text-purple' rel='noreferrer' target='_blank' href={mostRecentModifyUrl}>
{mostRecentModifyTimeStr}
</a>{' '}
using {mostRecentManagerName ? 'the ' : 'an '}
<a className='underline text-purple' rel='noreferrer' target='_blank' href={mostRecentManagerUrl}>
{mostRecentManagerName ?? 'unknown manager'}
</a>
</Text>
</HorizontalMetricCard>
)}
<HorizontalMetricCard label='Custom Tag'>
<Text size='M'>{marginAccount.userDataHex}</Text>
</HorizontalMetricCard>
{marginAccount.warningTime > 0 && (
<HorizontalMetricCard label='Liquidation Auction'>
<Text size='M'>{liquidationAuctionStr}</Text>
</HorizontalMetricCard>
)}
</MetricsGridLower>
</MetricsGrid>
);
Expand Down
4 changes: 3 additions & 1 deletion earn/src/components/advanced/modal/RemoveCollateralModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,9 @@ export default function RemoveCollateralModal(props: RemoveCollateralModalProps)
}, [isOpen, borrower.token0]);

const tokenOptions = [borrower.token0, borrower.token1];
const isToken0 = collateralToken.address === borrower.token0.address;
if (!tokenOptions.some((token) => token.equals(collateralToken))) return null;

const isToken0 = collateralToken.equals(borrower.token0);

const existingCollateral = isToken0 ? borrower.assets.amount0 : borrower.assets.amount1;
const collateralAmount = GN.fromDecimalString(collateralAmountStr || '0', collateralToken.decimals);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export function AddCollateralTab(props: AddCollateralTabProps) {
}, [marginAccount.token0]);

const tokenOptions = [marginAccount.token0, marginAccount.token1];
if (!tokenOptions.some((token) => token.equals(collateralToken))) return null;

const isToken0 = collateralToken.address === marginAccount.token0.address;

Expand Down
11 changes: 11 additions & 0 deletions earn/src/data/BalanceSheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,3 +357,14 @@ export function computeLTV(iv: number, nSigma: number) {
const ltv = 1 / ((1 + 1 / ALOE_II_MAX_LEVERAGE + 1 / ALOE_II_LIQUIDATION_INCENTIVE) * Math.exp(iv * nSigma));
return Math.max(0.1, Math.min(ltv, 0.9));
}

const Q = 22.8811827075;
const R = 103567.889099532;
const S = 0.95;
const M = 20.405429;
const N = 7 * 24 * 60 * 60 - 5 * 60;

export function auctionCurve(auctionTimeSeconds: number) {
if (auctionTimeSeconds >= N) return Infinity;
return S + R / (N - auctionTimeSeconds) - Q / (M + auctionTimeSeconds);
}
6 changes: 6 additions & 0 deletions earn/src/data/BorrowerNft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ export type BorrowerNft = {
borrowerAddress: Address;
tokenId: string;
index: number;
mostRecentModify?: ethers.Event;
};

export type BorrowerNftBorrower = MarginAccount & {
tokenId: string;
index: number;
mostRecentModify?: ethers.Event;
};

type BorrowerNftFilterParams = {
Expand Down Expand Up @@ -66,6 +68,8 @@ export async function fetchListOfBorrowerNfts(

// Create a mapping from (borrowerAddress => managerSet), which we'll need for filtering
const borrowerManagerSets: Map<Address, Set<Address>> = new Map();
// Also store the most recent event (solely for displaying on the advanced paged)
const borrowerMostRecentManager: Map<Address, ethers.Event> = new Map();
modifys.forEach((modify) => {
const borrower = modify.args!['borrower'] as Address;
const manager = modify.args!['manager'] as Address;
Expand All @@ -75,6 +79,7 @@ export async function fetchListOfBorrowerNfts(
} else {
// If there's no managerSet yet, create one
borrowerManagerSets.set(borrower, new Set<Address>([manager]));
borrowerMostRecentManager.set(borrower, modify);
}
});
orderedTokenIdStrs.forEach((orderedTokenIdStr) => {
Expand Down Expand Up @@ -152,6 +157,7 @@ export async function fetchListOfBorrowerNfts(
borrowerAddress: borrower,
tokenId,
index,
mostRecentModify: borrowerMostRecentManager.get(borrower),
};
})
);
Expand Down
23 changes: 17 additions & 6 deletions earn/src/data/MarginAccount.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Big from 'big.js';
import { ContractCallContext, Multicall } from 'ethereum-multicall';
import { ethers } from 'ethers';
import { BigNumber, ethers } from 'ethers';
import JSBI from 'jsbi';
import { borrowerAbi } from 'shared/lib/abis/Borrower';
import { borrowerLensAbi } from 'shared/lib/abis/BorrowerLens';
Expand Down Expand Up @@ -73,6 +73,8 @@ export type MarginAccount = {
lender1: Address;
iv: number;
nSigma: number;
userDataHex: `0x${string}`;
warningTime: number;
};

/**
Expand Down Expand Up @@ -164,6 +166,11 @@ export async function fetchBorrowerDatas(
methodName: 'LENDER1',
methodParameters: [],
},
{
reference: 'slot0',
methodName: 'slot0',
methodParameters: [],
},
{
reference: 'getLiabilities',
methodName: 'getLiabilities',
Expand Down Expand Up @@ -283,7 +290,7 @@ export async function fetchBorrowerDatas(
const feeTier = NumericFeeTierToEnum(fee);
const token0 = getToken(chainId, token0Address)!;
const token1 = getToken(chainId, token1Address)!;
const liabilitiesData = accountReturnContexts[2].returnValues;
const liabilitiesData = accountReturnContexts[3].returnValues;
const token0Balance = token0ReturnContexts[0].returnValues[0];
const token1Balance = token1ReturnContexts[0].returnValues[0];
const healthData = lensReturnContexts[0].returnValues;
Expand Down Expand Up @@ -315,8 +322,10 @@ export async function fetchBorrowerDatas(
uniswapPositions
);

const lender0 = accountReturnContexts[0].returnValues[0];
const lender1 = accountReturnContexts[1].returnValues[0];
const slot0 = accountReturnContexts[2].returnValues[0] as BigNumber;
const userDataHex = slot0.shr(144).mask(64).toHexString() as `0x${string}`;
const warningTime = slot0.shr(208).mask(40).toNumber();

const oracleReturnValues = convertBigNumbersForReturnContexts(oracleResults.callsReturnContext)[0].returnValues;
const marginAccount: MarginAccount = {
address: accountAddress,
Expand All @@ -329,9 +338,11 @@ export async function fetchBorrowerDatas(
health,
token0,
token1,
lender0,
lender1,
lender0: accountReturnContexts[0].returnValues[0],
lender1: accountReturnContexts[1].returnValues[0],
nSigma,
userDataHex,
warningTime,
};
marginAccounts.push(marginAccount);
});
Expand Down
2 changes: 2 additions & 0 deletions earn/src/data/Uniboost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,8 @@ export async function fetchBoostBorrower(
lender1,
iv,
nSigma,
userDataHex: '0x',
warningTime: 0,
};

return {
Expand Down
1 change: 1 addition & 0 deletions earn/src/pages/AdvancedPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ export default function AdvancedPage() {
...borrower,
tokenId: borrowerNfts[i].tokenId,
index: borrowerNfts[i].index,
mostRecentModify: borrowerNfts[i].mostRecentModify,
}));
setBorrowerNftBorrowers(fetchedBorrowerNftBorrowers);
})();
Expand Down
2 changes: 2 additions & 0 deletions earn/src/pages/boost/ImportBoostPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ export default function ImportBoostPage() {
lender1: '0x',
iv,
nSigma,
userDataHex: '0x',
warningTime: 0,
},
{
...position,
Expand Down
13 changes: 13 additions & 0 deletions shared/src/data/constants/ChainSpecific.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ export const APPROX_SECONDS_PER_BLOCK: { [chainId: number]: number } = {
[base.id]: 2,
};

export const MANAGER_NAME_MAP: { [manager: Address]: string } = {
'0xBb5A35B80b15A8E5933fDC11646A20f6159Dd061': 'SimpleManager',
'0x2b7E3A41Eac757CC1e8e9E61a4Ad5C9D6421516e': 'BorrowerNFTMultiManager',
'0xA07FD687882FfE7380A044e7542bDAc6F8672Bf7': 'BorrowerNFTSimpleManager',
'0xe1Bf15D99330E684020622856916F854c9322CB6': 'BorrowerNFTWithdrawManager',
'0x3EE236D69F6950525ff317D7a872439F09902C65': 'UniswapNFTManager',
'0x7357E37a60839DE89A52861Cf50851E317FFBE71': 'UniswapNFTManager',
'0x3Bb9F64b0e6b15dD5792A008c06E5c4Dc9d23D8f': 'FrontendManager',
'0xB6B7521cd3bd116432FeD94c2262Dd02BA616Db4': 'BoostManager',
'0x8E287b280671700EBE66A908A56C648f930b73b4': 'BoostManager',
'0x6BDa468b1d473028938585a04eC3c62dcFF5309B': 'Permit2Manager',
};

export const MULTICALL_ADDRESS: { [chainId: number]: Address } = {
[mainnet.id]: '0xcA11bde05977b3631167028862bE2a173976CA11',
[optimism.id]: '0xcA11bde05977b3631167028862bE2a173976CA11',
Expand Down

0 comments on commit 529ec04

Please sign in to comment.