Skip to content
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
18 changes: 4 additions & 14 deletions ui/ts/components/CollateralizationMetricField.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,25 @@
import { CurrencyValue } from './CurrencyValue.js'
import { MetricField } from './MetricField.js'
import { getRepPriceSourceCopy, renderRepPriceSourceLabel, type RepPriceSource } from '../lib/repPriceSource.js'
import { getCollateralizationDisplayState, getCollateralizationTone } from '../lib/trading.js'

type UniswapPriceSource = 'v4' | 'v3' | 'mock'

type CollateralizationMetricFieldProps = {
className?: string | undefined
collateralizationPercent: bigint | undefined
repPerEthSource: UniswapPriceSource | undefined
repPerEthSource: RepPriceSource | undefined
repPerEthSourceUrl: string | undefined
securityBondAllowance: bigint | undefined
securityMultiplier: bigint | undefined
}

function renderSourceLink(source: UniswapPriceSource, sourceUrl: string | undefined) {
const label = source === 'mock' ? 'MOCK' : `u${source === 'v4' ? '4' : '3'}`
if (sourceUrl === undefined) return `(${label})`
return (
<a href={sourceUrl} title={source === 'v4' ? 'Price from Uniswap V4' : 'Price from Uniswap V3'} target='_blank' rel='noreferrer'>
{`(${label})`}
</a>
)
}

export function CollateralizationMetricField({ className, collateralizationPercent, repPerEthSource, repPerEthSourceUrl, securityBondAllowance, securityMultiplier }: CollateralizationMetricFieldProps) {
const displayState = getCollateralizationDisplayState(securityBondAllowance, collateralizationPercent)
const tone = displayState === 'noActiveAllowance' ? undefined : getCollateralizationTone(collateralizationPercent, securityMultiplier)
const valueClassName = tone === 'success' ? 'metric-value-success' : tone === 'danger' ? 'metric-value-danger' : undefined
const repPriceSourceCopy = getRepPriceSourceCopy(repPerEthSource)

return (
<MetricField className={className} label={<span title='Uses the live Uniswap REP/ETH quote.'>Collateralization {repPerEthSource === undefined ? undefined : renderSourceLink(repPerEthSource, repPerEthSourceUrl)}</span>} valueClassName={valueClassName}>
<MetricField className={className} label={<span title={repPriceSourceCopy.tooltip}>Collateralization {renderRepPriceSourceLabel(repPerEthSource, repPerEthSourceUrl)}</span>} valueClassName={valueClassName}>
{displayState === 'noActiveAllowance' ? 'No active allowance' : displayState === 'unavailable' ? 'Awaiting REP/ETH price' : <CurrencyValue value={collateralizationPercent} suffix='%' copyable={false} />}
</MetricField>
)
Expand Down
38 changes: 22 additions & 16 deletions ui/ts/components/LiquidationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { sameAddress } from '../lib/address.js'
import { formatCurrencyInputBalance } from '../lib/formatters.js'
import { getLiquidationFailureReason, simulateLiquidation } from '../lib/liquidation.js'
import { parseRepAmountInput } from '../lib/marketForm.js'
import { getRepPriceSourceCopy, renderRepPriceSourceLabel, type RepPriceSource } from '../lib/repPriceSource.js'
import { getCollateralizationTone, getVaultCollateralizationPercent } from '../lib/trading.js'
import type { ListedSecurityPool, OracleManagerDetails, SecurityPoolOverviewActionResult, SecurityPoolVaultSummary } from '../types/contracts.js'

Expand All @@ -29,7 +30,7 @@ type LiquidationModalProps = {
onLoadPoolOracleManager: (managerAddress: Address) => void
onSelectedPoolViewChange: (view: string | undefined) => void
repPerEthPrice: bigint | undefined
repPerEthSource: 'v4' | 'v3' | 'mock' | undefined
repPerEthSource: RepPriceSource | undefined
repPerEthSourceUrl: string | undefined
selectedPool: ListedSecurityPool | undefined
securityPoolOverviewActiveAction: 'queueLiquidation' | undefined
Expand Down Expand Up @@ -68,17 +69,6 @@ function getLiquidationButtonLabels(currentPoolOracleManagerDetails: OracleManag
}
}

function renderPriceSourceLabel(source: 'v4' | 'v3' | 'mock' | undefined, sourceUrl: string | undefined) {
if (source === undefined) return undefined
const label = source === 'mock' ? 'MOCK' : `u${source === 'v4' ? '4' : '3'}`
if (sourceUrl === undefined) return `(${label})`
return (
<a href={sourceUrl} title={source === 'v4' ? 'Price from Uniswap V4' : 'Price from Uniswap V3'} target='_blank' rel='noreferrer'>
{`(${label})`}
</a>
)
}

function getCollateralizationValueClassName(collateralizationPercent: bigint | undefined, securityMultiplier: bigint | undefined) {
const tone = getCollateralizationTone(collateralizationPercent, securityMultiplier)
return tone === 'success' ? 'metric-value-success' : tone === 'danger' ? 'metric-value-danger' : undefined
Expand Down Expand Up @@ -164,8 +154,9 @@ export function LiquidationModal({
const poolOraclePrice = currentPoolOracleManagerDetails?.lastPrice ?? selectedPool?.lastOraclePrice
const poolOracleSettlementTimestamp = currentPoolOracleManagerDetails?.lastSettlementTimestamp ?? selectedPool?.lastOracleSettlementTimestamp ?? 0n
const poolOracleCollateralization = targetVaultSummary === undefined ? undefined : getVaultCollateralizationPercent(targetVaultSummary.repDepositShare, targetVaultSummary.securityBondAllowance, poolOraclePrice)
const uniswapCollateralization = targetVaultSummary === undefined ? undefined : getVaultCollateralizationPercent(targetVaultSummary.repDepositShare, targetVaultSummary.securityBondAllowance, repPerEthPrice)
const quotedPriceCollateralization = targetVaultSummary === undefined ? undefined : getVaultCollateralizationPercent(targetVaultSummary.repDepositShare, targetVaultSummary.securityBondAllowance, repPerEthPrice)
const callerPoolOracleCollateralization = callerVaultSummary === undefined ? undefined : getVaultCollateralizationPercent(callerVaultSummary.repDepositShare, callerVaultSummary.securityBondAllowance, poolOraclePrice)
const repPriceSourceCopy = getRepPriceSourceCopy(repPerEthSource)
const liquidationExecutionMode = getLiquidationExecutionMode(currentPoolOracleManagerDetails)
const buttonLabels = getLiquidationButtonLabels(currentPoolOracleManagerDetails)
const trimmedLiquidationTargetVault = liquidationTargetVault.trim()
Expand Down Expand Up @@ -310,9 +301,24 @@ export function LiquidationModal({
<MetricField label='Target Collateralization @ Open Oracle' valueClassName={getCollateralizationValueClassName(poolOracleCollateralization, selectedPool?.securityMultiplier)}>
{poolOracleCollateralization === undefined ? 'Unavailable' : <CurrencyValue value={poolOracleCollateralization} suffix='%' copyable={false} />}
</MetricField>
<MetricField label={<span>Uniswap REP / ETH {renderPriceSourceLabel(repPerEthSource, repPerEthSourceUrl)}</span>}>{repPerEthPrice === undefined ? 'Unavailable' : <CurrencyValue value={repPerEthPrice} suffix='REP / ETH' copyable={false} />}</MetricField>
<MetricField label={<span>Target Collateralization @ Uniswap {renderPriceSourceLabel(repPerEthSource, repPerEthSourceUrl)}</span>} valueClassName={getCollateralizationValueClassName(uniswapCollateralization, selectedPool?.securityMultiplier)}>
{uniswapCollateralization === undefined ? 'Unavailable' : <CurrencyValue value={uniswapCollateralization} suffix='%' copyable={false} />}
<MetricField
label={
<span>
{repPriceSourceCopy.quotedRepPerEthLabel} {renderRepPriceSourceLabel(repPerEthSource, repPerEthSourceUrl)}
</span>
}
>
{repPerEthPrice === undefined ? 'Unavailable' : <CurrencyValue value={repPerEthPrice} suffix='REP / ETH' copyable={false} />}
</MetricField>
<MetricField
label={
<span>
{repPriceSourceCopy.quotedCollateralizationLabel} {renderRepPriceSourceLabel(repPerEthSource, repPerEthSourceUrl)}
</span>
}
valueClassName={getCollateralizationValueClassName(quotedPriceCollateralization, selectedPool?.securityMultiplier)}
>
{quotedPriceCollateralization === undefined ? 'Unavailable' : <CurrencyValue value={quotedPriceCollateralization} suffix='%' copyable={false} />}
</MetricField>
<MetricField label='Caller Collateralization @ Open Oracle' valueClassName={getCollateralizationValueClassName(callerPoolOracleCollateralization, selectedPool?.securityMultiplier)}>
{callerPoolOracleCollateralization === undefined ? 'Unavailable' : <CurrencyValue value={callerPoolOracleCollateralization} suffix='%' copyable={false} />}
Expand Down
16 changes: 4 additions & 12 deletions ui/ts/components/OverviewPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DataGrid } from './DataGrid.js'
import { MetricField } from './MetricField.js'
import { StateHint } from './StateHint.js'
import { TransactionActionButton } from './TransactionActionButton.js'
import { renderRepPriceSourceLabel } from '../lib/repPriceSource.js'
import type { OverviewPanelsProps } from '../types/components.js'

export function OverviewPanels({
Expand All @@ -30,15 +31,6 @@ export function OverviewPanels({
const isWalletBootstrapLoading = !walletBootstrapComplete && accountState.address === undefined
const isWalletAddressLoading = isConnectingWallet || isWalletBootstrapLoading
const showAccountBalances = walletBootstrapComplete && accountState.address !== undefined
const renderSourceLink = (source: 'v3' | 'v4' | 'mock', sourceUrl: string | undefined) => {
const label = source === 'mock' ? 'MOCK' : `u${source === 'v4' ? '4' : '3'}`
if (sourceUrl === undefined) return `(${label})`
return (
<a href={sourceUrl} title={source === 'v4' ? 'Price from Uniswap V4' : 'Price from Uniswap V3'} target='_blank' rel='noreferrer'>
{`(${label})`}
</a>
)
}

return (
<section className='overview-shell'>
Expand Down Expand Up @@ -73,16 +65,16 @@ export function OverviewPanels({
<MetricField
label={
<span className='metric-label-with-action'>
<span>REP/ETH {repPerEthSource === undefined ? undefined : renderSourceLink(repPerEthSource, repPerEthSourceUrl)}</span>
<button type='button' className='quiet metric-label-refresh' onClick={onRefreshRepPrices} disabled={isLoadingRepPrices} aria-label='Refresh Uniswap prices' title={isLoadingRepPrices ? 'Refreshing Uniswap prices...' : 'Refresh Uniswap prices'}>
<span>REP/ETH {renderRepPriceSourceLabel(repPerEthSource, repPerEthSourceUrl)}</span>
<button type='button' className='quiet metric-label-refresh' onClick={onRefreshRepPrices} disabled={isLoadingRepPrices} aria-label='Refresh REP prices' title={isLoadingRepPrices ? 'Refreshing REP prices...' : 'Refresh REP prices'}>
</button>
</span>
}
>
<CurrencyValue value={repPerEthPrice} loading={isLoadingRepPrices} copyable={false} />
</MetricField>
<MetricField label={<>REP/USDC {repUsdcSource === undefined ? undefined : renderSourceLink(repUsdcSource, repUsdcSourceUrl)}</>}>
<MetricField label={<>REP/USDC {renderRepPriceSourceLabel(repUsdcSource, repUsdcSourceUrl)}</>}>
<CurrencyValue value={repUsdcPrice} loading={isLoadingRepPrices} suffix='USDC' units={6} />
</MetricField>
<MetricField label='Universe'>{universeLabel}</MetricField>
Expand Down
7 changes: 6 additions & 1 deletion ui/ts/lib/liquidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,12 @@ export function getLiquidationFailureReason({
targetVaultSummary,
})
if (simulation.debtToMove <= 0n) return 'This vault has no debt available to move.'
if (isVaultLiquidatable(repPerEthPrice, simulation.callerAfter.securityBondAllowance, simulation.callerAfter.repDepositShare, securityMultiplier)) return 'The caller vault would become liquidatable after this liquidation.'
if (isVaultLiquidatable(repPerEthPrice, simulation.callerAfter.securityBondAllowance, simulation.callerAfter.repDepositShare, securityMultiplier)) {
if (isVaultLiquidatable(repPerEthPrice, simulation.callerBefore.securityBondAllowance, simulation.callerBefore.repDepositShare, securityMultiplier)) {
return 'The caller vault would remain liquidatable after this liquidation.'
}
return 'The caller vault would become liquidatable after this liquidation.'
}
if (simulation.targetAfter.repDepositShare !== 0n && simulation.targetAfter.repDepositShare < MIN_REP_DEPOSIT) return 'The target vault would fall below the minimum REP collateral after liquidation.'
if (simulation.targetAfter.securityBondAllowance !== 0n && simulation.targetAfter.securityBondAllowance < MIN_SECURITY_BOND_DEBT) return 'The target vault would fall below the minimum security bond allowance after liquidation.'
if (simulation.callerAfter.repDepositShare < MIN_REP_DEPOSIT) return 'The caller vault would remain below the minimum REP collateral after liquidation.'
Expand Down
61 changes: 61 additions & 0 deletions ui/ts/lib/repPriceSource.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { assertNever } from './assert.js'

export type RepPriceSource = 'v4' | 'v3' | 'mock'

type RepPriceSourceCopy = {
badgeLabel: string | undefined
linkTitle: string | undefined
quotedCollateralizationLabel: string
quotedRepPerEthLabel: string
tooltip: string
}

export function getRepPriceSourceCopy(source: RepPriceSource | undefined): RepPriceSourceCopy {
switch (source) {
case 'mock':
return {
badgeLabel: 'MOCK',
linkTitle: 'Price from the simulation mock',
quotedCollateralizationLabel: 'Target Collateralization @ Simulation Price',
quotedRepPerEthLabel: 'Simulation REP / ETH',
tooltip: 'Uses the simulation REP/ETH mock price.',
}
case 'v4':
return {
badgeLabel: 'u4',
linkTitle: 'Price from Uniswap V4',
quotedCollateralizationLabel: 'Target Collateralization @ Uniswap V4 Price',
quotedRepPerEthLabel: 'Uniswap V4 REP / ETH',
tooltip: 'Uses the live Uniswap V4 REP/ETH quote.',
}
case 'v3':
return {
badgeLabel: 'u3',
linkTitle: 'Price from Uniswap V3',
quotedCollateralizationLabel: 'Target Collateralization @ Uniswap V3 Price',
quotedRepPerEthLabel: 'Uniswap V3 REP / ETH',
tooltip: 'Uses the live Uniswap V3 REP/ETH quote.',
}
case undefined:
return {
badgeLabel: undefined,
linkTitle: undefined,
quotedCollateralizationLabel: 'Target Collateralization',
quotedRepPerEthLabel: 'REP / ETH',
tooltip: 'REP/ETH price source is unavailable until a quote loads.',
}
default:
return assertNever(source)
}
}

export function renderRepPriceSourceLabel(source: RepPriceSource | undefined, sourceUrl: string | undefined) {
const copy = getRepPriceSourceCopy(source)
if (copy.badgeLabel === undefined) return undefined
if (sourceUrl === undefined || copy.linkTitle === undefined) return `(${copy.badgeLabel})`
return (
<a href={sourceUrl} title={copy.linkTitle} target='_blank' rel='noreferrer'>
{`(${copy.badgeLabel})`}
</a>
)
}
Loading
Loading