Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d6855ae
feat: urgent redemptions
cyril-dfi Feb 5, 2026
2876d9f
feat: trove selection table
cyril-dfi Feb 5, 2026
9b43841
refactor: remove unnecessary case
cyril-dfi Feb 5, 2026
cc6b8e1
refactor: use LinkTextButton
cyril-dfi Feb 6, 2026
fa5fea7
refactor: align capping with redeem screen
cyril-dfi Feb 6, 2026
2cb8c29
refactor: use redemption bonus const
cyril-dfi Feb 6, 2026
c1a6a7c
fix: align sorting
cyril-dfi Feb 6, 2026
0db18aa
refactor: use correct types
cyril-dfi Feb 6, 2026
fe67eb7
feat: remove warning
cyril-dfi Feb 6, 2026
1cd0ed7
fix: trove selection logic
cyril-dfi Feb 6, 2026
f789d38
feat: only allowed operation during shutdown mode is closing a loan
cyril-dfi Feb 6, 2026
8dd395a
feat: shutdown mode banner
cyril-dfi Feb 6, 2026
be09989
refactor: add fallback
cyril-dfi Feb 9, 2026
cea0750
refactor: align default max troves
cyril-dfi Feb 9, 2026
6e2b623
fix: revert dependency change
cyril-dfi Feb 9, 2026
f59eb09
doc: rename "urgent redemptions" to "shutdown redemptions"
cyril-dfi Feb 9, 2026
73487c8
doc: add link to liquity docs
cyril-dfi Feb 12, 2026
ae57de2
chore: add missing redemption helper to generated .env
danielattilasimon Mar 8, 2026
2403c99
fix: use fetchPrice during shutdown instead of lastGoodPrice
cyril-dfi Mar 9, 2026
e2db01a
refactor: remove dead useLastGoodPrice function
cyril-dfi Mar 9, 2026
95c49c1
doc: change shutdown mode banner to yellow
cyril-dfi Mar 9, 2026
2991b60
Merge remote-tracking branch 'origin/main' into urgent-redemptions
danielattilasimon May 2, 2026
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
3 changes: 3 additions & 0 deletions contracts/utils/deployment-manifest-to-app-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const ZDeploymentManifest = z.object({
debtInFrontHelper: ZAddress,
exchangeHelpers: ZAddress,
exchangeHelpersV2: ZAddress,
redemptionHelper: ZAddress,

governance: z.object({
LUSDToken: ZAddress,
Expand Down Expand Up @@ -189,6 +190,8 @@ function contractNameToAppEnvVariable(contractName: string, prefix: string = "")
return `${prefix}_EXCHANGE_HELPERS`;
case "exchangeHelpersV2":
return `${prefix}_EXCHANGE_HELPERS_V2`;
case "redemptionHelper":
return `${prefix}_REDEMPTION_HELPER`;

// collateral contracts
case "activePool":
Expand Down
5 changes: 5 additions & 0 deletions frontend/app/src/app/redeem/shutdown/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { UrgentRedeemScreen } from "@/src/screens/UrgentRedeemScreen/UrgentRedeemScreen";

export default function Page() {
return <UrgentRedeemScreen />;
}
2 changes: 2 additions & 0 deletions frontend/app/src/comps/AppLayout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { ReactNode } from "react";
import { Banner } from "@/Banner";
import { LegacyPositionsBanner } from "@/src/comps/LegacyPositionsBanner/LegacyPositionsBanner";
import { SafetyModeBanner } from "@/src/comps/SafetyModeBanner/SafetyModeBanner";
import { ShutdownModeBanner } from "@/src/comps/ShutdownModeBanner/ShutdownModeBanner";
import { SubgraphDownBanner } from "@/src/comps/SubgraphDownBanner/SubgraphDownBanner";
import { V1StabilityPoolBanner } from "@/src/comps/V1StabilityPoolBanner/V1StabilityPoolBanner";
import { V1StakingBanner } from "@/src/comps/V1StakingBanner/V1StakingBanner";
Expand Down Expand Up @@ -45,6 +46,7 @@ export function AppLayout({
{LEGACY_CHECK && <LegacyPositionsBanner />}
{SUBGRAPH_CHECK && <SubgraphDownBanner />}
{SAFETY_MODE_CHECK && <SafetyModeBanner />}
{SAFETY_MODE_CHECK && <ShutdownModeBanner />}
<div
className={css({
display: "flex",
Expand Down
31 changes: 31 additions & 0 deletions frontend/app/src/comps/InfoBox/InfoBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { ReactNode } from "react";

import { css } from "@/styled-system/css";

export function InfoBox(props: {
title?: ReactNode;
children?: ReactNode;
}) {
return (
<section
className={css({
display: "flex",
flexDirection: "column",
gap: 8,
padding: 16,
color: "infoSurfaceContent",
background: "infoSurface",
border: "1px solid token(colors.infoSurfaceBorder)",
borderRadius: 8,
})}
>
{props.title && (
<header className={css({ display: "flex", flexDirection: "column", fontSize: 16 })}>
<h1 className={css({ fontWeight: 600 })}>{props.title}</h1>
</header>
)}

{props.children}
</section>
);
}
13 changes: 10 additions & 3 deletions frontend/app/src/comps/SafetyModeBanner/SafetyModeBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
"use client";

import { InfoBanner } from "@/src/comps/InfoBanner/InfoBanner";
import { useSafetyMode } from "@/src/liquity-utils";
import { useSafetyMode, useShutdownStatus } from "@/src/liquity-utils";
import { token } from "@/styled-system/tokens";
import { IconWarning } from "@liquity2/uikit";

export function SafetyModeBanner() {
const safetyMode = useSafetyMode();
const shutdownStatus = useShutdownStatus();

const shutdownBranchIds = new Set(
shutdownStatus.data?.filter((b) => b.isShutdown).map((b) => b.branchId) ?? [],
);

const branchesInSafetyMode = (safetyMode.data?.branchesInSafetyMode ?? [])
.filter((b) => !shutdownBranchIds.has(b.branchId));

const branchesInSafetyMode = safetyMode.data?.branchesInSafetyMode ?? [];
const branchNames = branchesInSafetyMode.map((b) => b.symbol).join(", ");

return (
<InfoBanner
show={Boolean(safetyMode.data?.isAnySafetyMode)}
show={branchesInSafetyMode.length > 0}
icon={<IconWarning size={16} />}
messageDesktop={
<>
Expand Down
32 changes: 32 additions & 0 deletions frontend/app/src/comps/ShutdownModeBanner/ShutdownModeBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use client";

import { InfoBanner } from "@/src/comps/InfoBanner/InfoBanner";
import { getBranch, useShutdownStatus } from "@/src/liquity-utils";
import { token } from "@/styled-system/tokens";
import { IconWarning } from "@liquity2/uikit";

export function ShutdownModeBanner() {
const shutdownStatus = useShutdownStatus();

const branchesInShutdown = shutdownStatus.data?.filter((b) => b.isShutdown) ?? [];
const branchNames = branchesInShutdown.map((b) => getBranch(b.branchId).symbol).join(", ");

return (
<InfoBanner
show={branchesInShutdown.length > 0}
icon={<IconWarning size={16} />}
messageDesktop={
<>
The {branchNames} branch{branchesInShutdown.length > 1 ? "es are" : " is"} in Shutdown Mode.
You can only close positions or redeem BOLD.
</>
}
linkLabel="Learn more"
linkLabelMobile="Shutdown Mode active"
linkHref="https://docs.liquity.org/v2-faq/borrowing-and-liquidations#what-is-shutdown-mode"
linkExternal
backgroundColor={token("colors.brandGolden")}
foregroundColor={token("colors.brandGoldenContent")}
/>
);
}
92 changes: 92 additions & 0 deletions frontend/app/src/content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,98 @@ export default {
</>
),
},
shutdownWarning: {
title: "Branch Shutdown",
borrowMessage: (collName: string) => (
<>
The {collName} branch is in shutdown mode. New loans cannot be opened on this branch.
</>
),
loanMessage: (collName: string) => (
<>
The {collName} branch is in shutdown mode. Loan adjustments are not available. You can only close your loan.
</>
),
},
urgentRedeemScreen: {
headingTitle: "Shutdown Redemptions",
headingTitleActive: "Shutdown Redemption",
selectBranchLabel: "Select branch",
redeemFieldLabel: "You redeem",
insufficientBalance: (balance: string) => `Insufficient BOLD balance. You have ${balance} BOLD.`,
amountCapped: (amount: string) => `Capped to ${amount} BOLD (max amount redeemable).`,
youReceive: "You receive",
bonusLabel: (bonusPct: string) => `Including ${bonusPct} bonus`,
bonusTooltip: (bonusPct: string) => `Shutdown redemptions include a ${bonusPct} bonus on the collateral received.`,
slippageTolerance: "Slippage tolerance",
manualTrovesLabel: "Manually selected troves",
autoTrovesLabel: "Auto-selected troves",
useAutoSelection: "Use auto-selection",
manuallySelectTroves: "Manually select troves",
trovesCount: (count: number) => `${count} ${count === 1 ? "trove" : "troves"} will be used for this redemption.`,
action: "Redeem",
backLink: "Back",
successLink: "Go to the Dashboard",
successMessage: "The shutdown redemption was successful.",
noShutdown: {
title: "No Branches in Shutdown Mode",
body: (
<>
Shutdown redemptions are only available when a branch is in shutdown mode. Currently, all branches are
operating normally.
</>
),
link: "Go to standard redemptions",
},
noTroves: {
title: "No Shutdown Redemptions Available",
body: "No troves are currently available for shutdown redemption in this branch.",
},
troveTable: {
trovesSelected: (count: number) => `${count} ${count === 1 ? "trove" : "troves"} selected`,
totalDebt: "Total debt:",
totalDebtUnit: "BOLD",
totalColl: "Total coll:",
deselectAll: "Clear all",
selectAllOnPage: "Select all on page",
clearSelection: "Clear selection",
columnTroveId: "Trove ID",
columnCollateral: "Collateral",
columnDebt: "Debt",
columnIcr: "ICR",
noTrovesAvailable: "No troves available",
page: (current: number, total: number) => `Page ${current} of ${total}`,
previous: "Previous",
next: "Next",
},
txFlow: {
title: "Review & Send Transaction",
youRedeemBold: "You redeem BOLD",
redeemTooltip: (bonusPct: string) =>
`Shutdown redemptions have 0% fee and include a ${bonusPct} collateral bonus.`,
youReceiveToken: (tokenName: string) => `You receive ${tokenName}`,
receiveTooltip: (tokenName: string, bonusPct: string) =>
`This is the estimated amount of ${tokenName} you will receive, including`
+ ` the ${bonusPct} bonus. The actual amount may vary based on the selected troves.`,
trovesLabel: "Troves to redeem from",
trovesTooltip: (
<>
The number of troves that will be used for this redemption. Shutdown redemptions are competitive - other users
may redeem from these troves before your transaction confirms.
</>
),
trovesValue: (count: number) => `${count} ${count === 1 ? "trove" : "troves"}`,
slippageTooltip: (threshold: N) => (
<>
If the actual collateral received is less than {threshold}{" "}
of the expected amount, the transaction will revert.
</>
),
approveStep: "Approve BOLD",
redeemStep: "Execute Shutdown Redemption",
},
},

manualLoanIdInput: {
title: "Data API error",
description:
Expand Down
94 changes: 94 additions & 0 deletions frontend/app/src/liquity-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1920,3 +1920,97 @@ export function useSafetyMode() {
enabled: Boolean(allRatios.data),
});
}

export type ShutdownStatus = {
branchId: BranchId;
isShutdown: boolean;
};

export function useShutdownStatus() {
const branches = getBranches();

return useReadContracts({
contracts: branches.map((branch) => ({
...branch.contracts.TroveManager,
functionName: "shutdownTime" as const,
})),
allowFailure: false,
query: {
refetchInterval: 12_000,
select: (results): ShutdownStatus[] => {
return results.map((shutdownTime, index) => {
const branch = branches[index];
if (!branch) {
throw new Error(`Branch at index ${index} not found`);
}
return {
branchId: branch.branchId,
isShutdown: Number(shutdownTime) > 0,
};
});
},
},
});
}

export function useIsBranchShutdown(branchId: BranchId) {
const shutdownStatus = useShutdownStatus();
return {
...shutdownStatus,
data: shutdownStatus.data?.find((s) => s.branchId === branchId)?.isShutdown ?? false,
};
}

export type RedeemableTrove = {
id: string;
troveId: TroveId;
debt: Dnum;
coll: Dnum;
stake: Dnum;
interestRate: Dnum;
};

async function fetchRedeemableTroves(
wagmiConfig: WagmiConfig,
branchId: BranchId,
maxTroves: number,
): Promise<RedeemableTrove[]> {
const MultiTroveGetter = getProtocolContract("MultiTroveGetter");

const troves = await readContract(wagmiConfig, {
...MultiTroveGetter,
functionName: "getMultipleSortedTroves",
args: [BigInt(branchId), -1n, BigInt(maxTroves)],
});

return troves
.filter((trove) => trove.entireDebt > 0n)
.map((trove) => ({
id: getPrefixedTroveId(branchId, `0x${trove.id.toString(16)}` as TroveId),
troveId: `0x${trove.id.toString(16)}` as TroveId,
debt: dnum18(trove.entireDebt),
coll: dnum18(trove.entireColl),
stake: dnum18(trove.stake),
interestRate: dnum18(trove.annualInterestRate),
}));
}

export function useRedeemableTroves(
branchId: BranchId | null,
options?: { first?: number },
) {
const wagmiConfig = useWagmiConfig();
const maxTroves = options?.first ?? 200;

return useQuery<RedeemableTrove[]>({
queryKey: ["redeemableTroves", branchId, maxTroves],
queryFn: async () => {
if (branchId === null) {
return [];
}
return fetchRedeemableTroves(wagmiConfig, branchId, maxTroves);
},
enabled: branchId !== null,
refetchInterval: 12_000,
});
}
28 changes: 27 additions & 1 deletion frontend/app/src/screens/BorrowScreen/BorrowScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
getCollToken,
useBranchCollateralRatios,
useBranchDebt,
useIsBranchShutdown,
useNextOwnerIndex,
usePredictOpenTroveUpfrontFee,
useRedemptionRiskOfInterestRate,
Expand Down Expand Up @@ -100,6 +101,7 @@ export function BorrowScreen() {

const balances = useBalances(account.address, KNOWN_COLLATERAL_SYMBOLS);
const collateralRatios = useBranchCollateralRatios(branch.id);
const isShutdown = useIsBranchShutdown(branch.id);

const collBalance = balances[collateral.symbol];
if (!collBalance) {
Expand Down Expand Up @@ -219,6 +221,7 @@ export function BorrowScreen() {

const isDelegated = interestRateMode === "delegate" && interestRateDelegate;
const allowSubmit = account.isConnected
&& !isShutdown.data
&& deposit.parsed
&& dn.gt(deposit.parsed, 0)
&& debt.parsed
Expand Down Expand Up @@ -520,7 +523,30 @@ export function BorrowScreen() {

<RedemptionInfo />

{isCcrConditionsNotMet && collateralRatios.data
{isShutdown.data
? (
<WarningBox>
<div>
<div
className={css({
fontSize: 16,
fontWeight: 600,
marginBottom: 12,
})}
>
{content.shutdownWarning.title}
</div>
<div
className={css({
fontSize: 15,
})}
>
{content.shutdownWarning.borrowMessage(collateral.name)}
</div>
</div>
</WarningBox>
)
: isCcrConditionsNotMet && collateralRatios.data
? (
<WarningBox>
<div>
Expand Down
Loading
Loading