From 7840a1f125de0eb0bff470d2f03025fd0a0ad266 Mon Sep 17 00:00:00 2001 From: t0rbik Date: Thu, 17 Oct 2024 13:06:48 +0200 Subject: [PATCH 1/2] add jusdc explain message, fix jusdc cap --- src/components/vaults/row/VaultAccordionRow.tsx | 16 +++++++++++----- src/components/vaults/row/VaultMessage.tsx | 10 +++++----- src/lib/config/metadataTypes.ts | 3 ++- src/lib/config/vaults.ts | 16 ++++++++++++---- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/components/vaults/row/VaultAccordionRow.tsx b/src/components/vaults/row/VaultAccordionRow.tsx index 053078ca..edb3d619 100644 --- a/src/components/vaults/row/VaultAccordionRow.tsx +++ b/src/components/vaults/row/VaultAccordionRow.tsx @@ -166,7 +166,8 @@ export const VaultAccordionRow = ({ vault }: { vault: Vault }) => {

TVL / Cap

@@ -314,18 +315,20 @@ export const CurrencyCell = ({ const VaultCapacityCell = ({ vault, - tokenDecimals = 18, + yieldTokenDecimals = 18, + underlyingTokenDecimals = 18, tokenSymbol, }: { vault: Vault; - tokenDecimals: number | undefined; + yieldTokenDecimals: number | undefined; + underlyingTokenDecimals: number | undefined; tokenSymbol: string | undefined; }) => { const chain = useChain(); const limitValue = formatUnits( vault.yieldTokenParams.maximumExpectedValue, - tokenDecimals, + yieldTokenDecimals, ); const { data: capacity, isPending } = useReadContract({ @@ -336,7 +339,10 @@ const VaultCapacityCell = ({ args: [vault.yieldToken, vault.yieldTokenParams.totalShares], query: { select: (currentValueBn) => { - const currentValue = formatUnits(currentValueBn, tokenDecimals); + const currentValue = formatUnits( + currentValueBn, + underlyingTokenDecimals, + ); const isFull = (parseFloat(currentValue) / parseFloat(limitValue)) * 100 >= 99; return { currentValue, isFull }; diff --git a/src/components/vaults/row/VaultMessage.tsx b/src/components/vaults/row/VaultMessage.tsx index 18e7f598..1e497ec2 100644 --- a/src/components/vaults/row/VaultMessage.tsx +++ b/src/components/vaults/row/VaultMessage.tsx @@ -10,12 +10,12 @@ const messageConfig = { } as const; export const VaultMessage = (props: { message: VaultMessageType }) => { - const { message, type, learnMoreUrl } = props.message; + const { message, type, linkHref, linkLabel } = props.message; return (
{

{message} - {learnMoreUrl && ( + {linkHref && ( <> - Learn more. + {linkLabel ?? "Learn more."} )} diff --git a/src/lib/config/metadataTypes.ts b/src/lib/config/metadataTypes.ts index f295bd81..9c55979b 100644 --- a/src/lib/config/metadataTypes.ts +++ b/src/lib/config/metadataTypes.ts @@ -44,7 +44,8 @@ export type MessageType = "info" | "warning" | "error"; export type VaultMessage = { message: string; type: MessageType; - learnMoreUrl?: string; + linkHref?: string; + linkLabel?: string; }; type ApiProvider = diff --git a/src/lib/config/vaults.ts b/src/lib/config/vaults.ts index fade3f3c..dadb3f9a 100644 --- a/src/lib/config/vaults.ts +++ b/src/lib/config/vaults.ts @@ -446,7 +446,7 @@ export const VAULTS: VaultsConfig = { message: "Yearn yvUSDC is currently disabled for underlying token deposit and withdraw.", type: "warning", - learnMoreUrl: "https://discord.com/invite/yearn", + linkHref: "https://discord.com/invite/yearn", }, ], gateway: "0xC02670867efac6D988F40878a5559a8D96002A56", @@ -471,7 +471,7 @@ export const VAULTS: VaultsConfig = { message: "Yearn yvDAI is currently disabled for underlying token deposit and withdraw.", type: "warning", - learnMoreUrl: "https://discord.com/invite/yearn", + linkHref: "https://discord.com/invite/yearn", }, ], gateway: "0xC02670867efac6D988F40878a5559a8D96002A56", @@ -533,7 +533,7 @@ export const VAULTS: VaultsConfig = { message: "Yearn yvWETH is currently disabled for underlying token deposit and withdraw.", type: "warning", - learnMoreUrl: "https://discord.com/invite/yearn", + linkHref: "https://discord.com/invite/yearn", }, ], gateway: "0xedE36d3F423EF198abE82D2463E0a18bcF2d9397", @@ -574,7 +574,15 @@ export const VAULTS: VaultsConfig = { underlyingSymbol: "USDC", yieldSymbol: "jUSDC", image: "jUSDC.webp", - messages: [], + messages: [ + { + type: "info", + message: + "Only jUSDC deposit and withdraw are available. Get jUSDC from Jones.", + linkHref: "https://app.jonesdao.io/vaults/leveraged/usdc", + linkLabel: "Get jUSDC.", + }, + ], api: { apr: getJonesApy, yieldType: "APY", From f516be20ea39627287c6fcdc3cb1e9ae50d130fb Mon Sep 17 00:00:00 2001 From: t0rbik Date: Thu, 17 Oct 2024 20:27:52 +0200 Subject: [PATCH 2/2] update liqudation to follow withdraw shares overflow workaroun --- .../common/input/LiquidateInput.tsx | 109 ++--------------- .../vaults/common_actions/Liquidate.tsx | 44 ++++++- .../useVaultsLiquidateAvailableBalance.ts | 112 ++++++++++++++++++ 3 files changed, 161 insertions(+), 104 deletions(-) create mode 100644 src/lib/queries/vaults/useVaultsLiquidateAvailableBalance.ts diff --git a/src/components/common/input/LiquidateInput.tsx b/src/components/common/input/LiquidateInput.tsx index 6abfb01b..47f38676 100644 --- a/src/components/common/input/LiquidateInput.tsx +++ b/src/components/common/input/LiquidateInput.tsx @@ -1,11 +1,9 @@ -import { formatUnits, zeroAddress } from "viem"; -import { useAccount, useReadContract } from "wagmi"; -import { useChain } from "@/hooks/useChain"; -import { Vault } from "@/lib/types"; -import { alchemistV2Abi } from "@/abi/alchemistV2"; -import { useWatchQuery } from "@/hooks/useWatchQuery"; +import { zeroAddress } from "viem"; + import { TokenInput } from "./TokenInput"; -import { ScopeKeys } from "@/lib/queries/queriesSchema"; + +import { Vault } from "@/lib/types"; +import { useVaultsLiquidateAvailableBalance } from "@/lib/queries/vaults/useVaultsLiquidateAvailableBalance"; export const LiquidateTokenInput = ({ amount, @@ -20,100 +18,9 @@ export const LiquidateTokenInput = ({ tokenSymbol: string; tokenDecimals: number; }) => { - const chain = useChain(); - const { address } = useAccount(); - - const { data: unrealizedDebt } = useReadContract({ - address: vault.alchemist.address, - abi: alchemistV2Abi, - chainId: chain.id, - functionName: "accounts", - args: [address ?? zeroAddress], - query: { - enabled: !!address, - select: ([debt]) => (debt < 0n ? 0n : debt), - }, - }); - - const { data: normalizedDebtToUnderlying } = useReadContract({ - address: vault.alchemist.address, - abi: alchemistV2Abi, - chainId: chain.id, - functionName: "normalizeDebtTokensToUnderlying", - args: [vault.underlyingToken, unrealizedDebt ?? 0n], - query: { - enabled: unrealizedDebt !== undefined, - }, - }); - - const { data: maximumShares } = useReadContract({ - address: vault.alchemist.address, - abi: alchemistV2Abi, - chainId: chain.id, - functionName: "convertUnderlyingTokensToShares", - args: [vault.yieldToken, normalizedDebtToUnderlying ?? 0n], - scopeKey: ScopeKeys.LiquidateInput, - query: { - enabled: normalizedDebtToUnderlying !== undefined, - }, - }); - - const { data: debtInYield } = useReadContract({ - address: vault.alchemist.address, - chainId: chain.id, - abi: alchemistV2Abi, - functionName: "convertSharesToYieldTokens", - args: [vault.yieldToken, maximumShares ?? 0n], - query: { - enabled: maximumShares !== undefined, - select: (balance) => - formatUnits(balance, vault.yieldTokenParams.decimals), - }, - }); - - const { data: sharesBalance } = useReadContract({ - address: vault.alchemist.address, - chainId: chain.id, - abi: alchemistV2Abi, - functionName: "positions", - args: [address ?? zeroAddress, vault.yieldToken], - scopeKey: ScopeKeys.LiquidateInput, - query: { - enabled: !!address, - select: ([shares]) => shares, - }, - }); - - const { data: balance } = useReadContract({ - address: vault.alchemist.address, - chainId: chain.id, - abi: alchemistV2Abi, - functionName: "convertSharesToYieldTokens", - args: [vault.yieldToken, sharesBalance ?? 0n], - scopeKey: ScopeKeys.LiquidateInput, - query: { - enabled: sharesBalance !== undefined, - select: (balance) => - formatUnits(balance, vault.yieldTokenParams.decimals), - }, - }); - - /** - * NOTE: Watch queries for changes in maximumShares, sharesBalance, and balance. - * maximumShares - because underlying tokens to shares uses price which changes each block; - * sharesBalance - if user deposited or withdrawed from vault for yield token; - * balance - because shares to yield token uses price which changes each block. - */ - useWatchQuery({ - scopeKey: ScopeKeys.LiquidateInput, - }); - - const externalMaximumAmount = - debtInYield !== undefined && - balance !== undefined && - +debtInYield < +balance - ? debtInYield - : undefined; + const { balance, externalMaximumAmount } = useVaultsLiquidateAvailableBalance( + { vault }, + ); return ( { const queryClient = useQueryClient(); const chain = useChain(); const mutationCallback = useWriteContractMutationCallback(); + const { address } = useAccount(); + const [amount, setAmount] = useState(""); const [slippage, setSlippage] = useState("0.5"); const [confirmedLiquidation, setConfirmedLiquidation] = useState(false); @@ -90,7 +94,7 @@ export const Liquidate = () => { v.yieldToken.toLowerCase() === liquidationTokenAddress?.toLowerCase(), ); - const { data: shares } = useReadContract({ + let { data: shares } = useReadContract({ address: vault?.alchemist.address, abi: alchemistV2Abi, chainId: chain.id, @@ -104,6 +108,33 @@ export const Liquidate = () => { }, }); + const { data: depositedSharesBalance } = useReadContract({ + address: vault?.alchemist.address, + abi: alchemistV2Abi, + chainId: chain.id, + functionName: "positions", + args: [address!, vault?.yieldToken ?? zeroAddress], + query: { + enabled: !!address, + select: ([shares]) => shares, + }, + }); + + const { balance } = useVaultsLiquidateAvailableBalance({ vault }); + + /** + * Guard against dynamic yield token price changes. + * When initially calculated max yield tokens amount from static shares, + * converts back to shares higher than deposited shares balance + */ + if ( + depositedSharesBalance !== undefined && + shares !== undefined && + depositedSharesBalance < shares + ) { + shares = depositedSharesBalance - 1n; + } + const { data: minimumOut } = useReadContract({ address: vault?.alchemist.address, abi: alchemistV2Abi, @@ -215,6 +246,13 @@ export const Liquidate = () => { } }; + const isInsufficientBalance = balance !== undefined && +amount > +balance; + const isDisabledCta = + isPending || + isInputZero(amount) || + !confirmedLiquidation || + isInsufficientBalance; + return (

{ variant="outline" width="full" onClick={onCtaClick} - disabled={isPending || isInputZero(amount) || !confirmedLiquidation} + disabled={isDisabledCta} > - Liquidate + {isInsufficientBalance ? "Insufficient Balance" : "Liquidate"} )} diff --git a/src/lib/queries/vaults/useVaultsLiquidateAvailableBalance.ts b/src/lib/queries/vaults/useVaultsLiquidateAvailableBalance.ts new file mode 100644 index 00000000..03b68221 --- /dev/null +++ b/src/lib/queries/vaults/useVaultsLiquidateAvailableBalance.ts @@ -0,0 +1,112 @@ +import { formatUnits, zeroAddress } from "viem"; +import { useAccount, useReadContract } from "wagmi"; + +import { alchemistV2Abi } from "@/abi/alchemistV2"; +import { ScopeKeys } from "@/lib/queries/queriesSchema"; +import { Vault } from "@/lib/types"; +import { useWatchQuery } from "@/hooks/useWatchQuery"; +import { useChain } from "@/hooks/useChain"; + +export const useVaultsLiquidateAvailableBalance = ({ + vault, +}: { + vault: Vault | undefined; +}) => { + const chain = useChain(); + const { address } = useAccount(); + + const { data: unrealizedDebt } = useReadContract({ + address: vault?.alchemist.address, + abi: alchemistV2Abi, + chainId: chain.id, + functionName: "accounts", + args: [address ?? zeroAddress], + scopeKey: ScopeKeys.LiquidateInput, + query: { + enabled: !!address, + select: ([debt]) => (debt < 0n ? 0n : debt), + }, + }); + + const { data: normalizedDebtToUnderlying } = useReadContract({ + address: vault?.alchemist.address, + abi: alchemistV2Abi, + chainId: chain.id, + functionName: "normalizeDebtTokensToUnderlying", + args: [vault?.underlyingToken ?? zeroAddress, unrealizedDebt ?? 0n], + query: { + enabled: !!vault?.underlyingToken && unrealizedDebt !== undefined, + }, + }); + + const { data: maximumShares } = useReadContract({ + address: vault?.alchemist.address, + abi: alchemistV2Abi, + chainId: chain.id, + functionName: "convertUnderlyingTokensToShares", + args: [vault?.yieldToken ?? zeroAddress, normalizedDebtToUnderlying ?? 0n], + query: { + enabled: !!vault?.yieldToken && normalizedDebtToUnderlying !== undefined, + }, + }); + + const { data: debtInYield } = useReadContract({ + address: vault?.alchemist.address, + chainId: chain.id, + abi: alchemistV2Abi, + functionName: "convertSharesToYieldTokens", + args: [vault?.yieldToken ?? zeroAddress, maximumShares ?? 0n], + query: { + enabled: !!vault?.yieldToken && maximumShares !== undefined, + select: (balance) => + formatUnits(balance, vault?.yieldTokenParams.decimals ?? 18), + }, + }); + + const { data: sharesBalance } = useReadContract({ + address: vault?.alchemist.address, + chainId: chain.id, + abi: alchemistV2Abi, + functionName: "positions", + args: [address ?? zeroAddress, vault?.yieldToken ?? zeroAddress], + scopeKey: ScopeKeys.LiquidateInput, + query: { + enabled: !!vault?.yieldToken && !!address, + select: ([shares]) => shares, + }, + }); + + const { data: balance } = useReadContract({ + address: vault?.alchemist.address, + chainId: chain.id, + abi: alchemistV2Abi, + functionName: "convertSharesToYieldTokens", + args: [vault?.yieldToken ?? zeroAddress, sharesBalance ?? 0n], + query: { + enabled: !!vault?.yieldToken && sharesBalance !== undefined, + select: (balance) => + formatUnits(balance, vault?.yieldTokenParams.decimals ?? 18), + }, + }); + + /** + * NOTE: Watch queries for changes in sharesBalance. + * sharesBalance - if user deposited or withdrawed from vault for yield token; + * unrealizedDebt - if user borrowed or repayed from alchemist associated to vault for yield token; + */ + useWatchQuery({ + scopeKey: ScopeKeys.LiquidateInput, + }); + + const externalMaximumAmount = + debtInYield !== undefined && + balance !== undefined && + +debtInYield < +balance + ? debtInYield + : undefined; + + return { + externalMaximumAmount, + balance, + }; +};