Skip to content
Draft
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
22 changes: 20 additions & 2 deletions src/features/market-detail/components/borrowers-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,25 @@ import { MONARCH_PRIMARY } from '@/constants/chartColors';
import { useMarketBorrowers } from '@/hooks/useMarketBorrowers';
import { formatSimple } from '@/utils/balance';
import type { Market } from '@/utils/types';
import { LiquidateButton } from './liquidate-button';

type BorrowersTableProps = {
chainId: number;
market: Market;
minShares: string;
oraclePrice: bigint;
onOpenFiltersModal: () => void;
showLiquidateButton?: boolean;
};

export function BorrowersTable({ chainId, market, minShares, oraclePrice, onOpenFiltersModal }: BorrowersTableProps) {
export function BorrowersTable({
chainId,
market,
minShares,
oraclePrice,
onOpenFiltersModal,
showLiquidateButton = true,
}: BorrowersTableProps) {
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 10;

Expand Down Expand Up @@ -117,13 +126,14 @@ export function BorrowersTable({ chainId, market, minShares, oraclePrice, onOpen
<TableHead className="text-right">COLLATERAL</TableHead>
<TableHead className="text-right">LTV</TableHead>
<TableHead className="text-right">% OF BORROW</TableHead>
{showLiquidateButton && <TableHead className="text-right">ACTIONS</TableHead>}
</TableRow>
</TableHeader>
<TableBody className="table-body-compact">
{borrowersWithLTV.length === 0 && !isLoading ? (
<TableRow>
<TableCell
colSpan={5}
colSpan={showLiquidateButton ? 6 : 5}
className="text-center text-gray-400"
>
No borrowers found for this market
Expand Down Expand Up @@ -175,6 +185,14 @@ export function BorrowersTable({ chainId, market, minShares, oraclePrice, onOpen
</TableCell>
<TableCell className="text-right text-sm">{borrower.ltv.toFixed(2)}%</TableCell>
<TableCell className="text-right text-sm">{percentDisplay}</TableCell>
{showLiquidateButton && (
<TableCell className="text-right">
<LiquidateButton
market={market}
borrower={borrower}
/>
</TableCell>
)}
</TableRow>
);
})
Expand Down
49 changes: 49 additions & 0 deletions src/features/market-detail/components/liquidate-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { useLiquidateTransaction } from '@/hooks/useLiquidateTransaction';
import type { Market } from '@/utils/types';

type Borrower = {
userAddress: string;
borrowAssets: string;
collateral: string;
ltv: number;
};

type LiquidateButtonProps = {
market: Market;
borrower: Borrower;
onSuccess?: () => void;
};

export function LiquidateButton({ market, borrower, onSuccess }: LiquidateButtonProps) {
// Seize all collateral - Morpho will calculate the appropriate repayment
const seizedAssets = BigInt(borrower.collateral);

const { isApproved, isLoading, approveAndLiquidate, signAndLiquidate } = useLiquidateTransaction({
market,
borrower,
seizedAssets,
onSuccess,
});
Comment on lines +19 to +28
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

No check for liquidation eligibility.

The button is rendered for all borrowers regardless of whether they're actually liquidatable (LTV > LLTV). Attempting to liquidate a healthy position will fail on-chain.

Consider disabling the button or checking eligibility:

Suggested approach
 export function LiquidateButton({ market, borrower, onSuccess }: LiquidateButtonProps) {
+  const maxLtv = Number(market.lltv) / 1e18;
+  const isLiquidatable = borrower.ltv > maxLtv * 100;
+
   // Seize all collateral - Morpho will calculate the appropriate repayment
   const seizedAssets = BigInt(borrower.collateral);

   // ... hook call ...

   return (
     <Button
       variant="ghost"
       size="sm"
       className="h-7 px-2 text-xs"
       onClick={handleClick}
-      disabled={isLoading}
+      disabled={isLoading || !isLiquidatable}
     >
-      {isLoading ? 'Processing...' : 'Liquidate'}
+      {isLoading ? 'Processing...' : isLiquidatable ? 'Liquidate' : 'Healthy'}
     </Button>
   );
 }
🤖 Prompt for AI Agents
In `@src/features/market-detail/components/liquidate-button.tsx` around lines 19 -
28, Add a liquidation-eligibility check inside the LiquidateButton component and
use it to disable/prevent actions when the borrower is not liquidatable: compute
a boolean (e.g., isLiquidatable) using borrower LTV versus the market
liquidation threshold/LLTV (referencing market.liquidationThreshold or
market.LLTV and borrower.ltv), pass that into the UI so the button is disabled
and shows a tooltip/message for healthy positions, and ensure the handlers from
useLiquidateTransaction (approveAndLiquidate, signAndLiquidate) early-return or
are not wired up when isLiquidatable is false to avoid on-chain failures.


const handleClick = useCallback(() => {
if (isApproved) {
void signAndLiquidate();
} else {
void approveAndLiquidate();
}
}, [isApproved, approveAndLiquidate, signAndLiquidate]);

return (
<Button
variant="default"
size="sm"
className="h-7 px-2 text-xs"
onClick={handleClick}
disabled={isLoading}
>
{isLoading ? 'Processing...' : 'Liquidate'}
</Button>
);
}
47 changes: 7 additions & 40 deletions src/features/market-detail/market-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@

import { useState, useCallback, useMemo } from 'react';
import { useParams } from 'next/navigation';
import { parseUnits, formatUnits, type Address, encodeFunctionData } from 'viem';
import { useConnection, useSwitchChain } from 'wagmi';
import morphoAbi from '@/abis/morpho';
import { parseUnits, formatUnits } from 'viem';
import { useConnection } from 'wagmi';
import { BorrowModal } from '@/modals/borrow/borrow-modal';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Spinner } from '@/components/ui/spinner';
Expand All @@ -17,7 +16,7 @@ import { useOraclePrice } from '@/hooks/useOraclePrice';
import { useTransactionFilters } from '@/stores/useTransactionFilters';
import { useMarketDetailPreferences, type MarketDetailTab } from '@/stores/useMarketDetailPreferences';
import useUserPosition from '@/hooks/useUserPosition';
import { useTransactionWithToast } from '@/hooks/useTransactionWithToast';
import { useAccrueInterest } from '@/hooks/useAccrueInterest';
import type { SupportedNetworks } from '@/utils/networks';
import { BorrowersTable } from '@/features/market-detail/components/borrowers-table';
import { BorrowsTable } from '@/features/market-detail/components/borrows-table';
Expand Down Expand Up @@ -98,17 +97,9 @@ function MarketContent() {
totalCount: suppliersTotalCount,
} = useAllMarketSuppliers(market?.uniqueKey, network);

const { mutateAsync: switchChainAsync } = useSwitchChain();

// Transaction hook for accruing interest
const { sendTransaction } = useTransactionWithToast({
toastId: 'accrue-interest',
pendingText: 'Accruing Interest',
successText: 'Interest Accrued',
errorText: 'Failed to accrue interest',
chainId: market?.morphoBlue.chain.id,
pendingDescription: 'Updating market interest rates...',
successDescription: 'Market interest rates have been updated',
const { accrueInterest } = useAccrueInterest({
market: market ?? undefined,
onSuccess: () => {
void refetchMarket();
},
Expand Down Expand Up @@ -265,30 +256,6 @@ function MarketContent() {
setShowBorrowModal(true);
};

const handleAccrueInterest = async () => {
await switchChainAsync({ chainId: market.morphoBlue.chain.id });
const morphoAddress = market.morphoBlue.address as Address;

sendTransaction({
to: morphoAddress,
account: address,
data: encodeFunctionData({
abi: morphoAbi,
functionName: 'accrueInterest',
args: [
{
loanToken: market.loanAsset.address as Address,
collateralToken: market.collateralAsset.address as Address,
oracle: market.oracleAddress as Address,
irm: market.irmAddress as Address,
lltv: BigInt(market.lltv),
},
],
}),
chainId: market.morphoBlue.chain.id,
});
};

return (
<>
<Header />
Expand All @@ -303,7 +270,7 @@ function MarketContent() {
allWarnings={allWarnings}
onSupplyClick={handleSupplyClick}
onBorrowClick={handleBorrowClick}
accrueInterest={handleAccrueInterest}
accrueInterest={accrueInterest}
/>

{showBorrowModal && (
Expand Down Expand Up @@ -421,7 +388,7 @@ function MarketContent() {
</div>

{/* Suppliers row: Pie + Concentration */}
<div className="grid gap-6 lg:grid-cols-2">
<div className="grid gap-6 lg:grid-cols-2 mt-6">
<SuppliersPieChart
chainId={network}
market={market}
Expand Down
59 changes: 59 additions & 0 deletions src/hooks/useAccrueInterest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useCallback } from 'react';
import { type Address, encodeFunctionData } from 'viem';
import { useConnection, useSwitchChain } from 'wagmi';
import morphoAbi from '@/abis/morpho';
import type { Market } from '@/utils/types';
import { useTransactionWithToast } from './useTransactionWithToast';

type UseAccrueInterestProps = {
market: Market | undefined;
onSuccess?: () => void;
};

export function useAccrueInterest({ market, onSuccess }: UseAccrueInterestProps) {
const { address: account } = useConnection();
const { mutateAsync: switchChainAsync } = useSwitchChain();

const { sendTransaction, isConfirming } = useTransactionWithToast({
toastId: 'accrue-interest',
pendingText: 'Accruing Interest',
successText: 'Interest Accrued',
errorText: 'Failed to accrue interest',
chainId: market?.morphoBlue.chain.id,
pendingDescription: 'Accruing interest...',
successDescription: 'Interest has been accrued',
onSuccess,
});

const accrueInterest = useCallback(async () => {
if (!market) return;

await switchChainAsync({ chainId: market.morphoBlue.chain.id });

const morphoAddress = market.morphoBlue.address as Address;

sendTransaction({
to: morphoAddress,
account,
data: encodeFunctionData({
abi: morphoAbi,
functionName: 'accrueInterest',
args: [
{
loanToken: market.loanAsset.address as Address,
collateralToken: market.collateralAsset.address as Address,
oracle: market.oracleAddress as Address,
irm: market.irmAddress as Address,
lltv: BigInt(market.lltv),
},
],
}),
chainId: market.morphoBlue.chain.id,
});
}, [market, account, switchChainAsync, sendTransaction]);

return {
accrueInterest,
isLoading: isConfirming,
};
}
Loading