Skip to content

Commit 86bb3b3

Browse files
feat: better message for disabled e-modes (#2910)
1 parent 484d823 commit 86bb3b3

File tree

9 files changed

+180
-62
lines changed

9 files changed

+180
-62
lines changed

src/components/transactions/Emode/EmodeModalContent.tsx

Lines changed: 101 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { formatUserSummary } from '@aave/math-utils';
1+
import { formatUserSummary, valueToBigNumber } from '@aave/math-utils';
22
import { ArrowNarrowRightIcon } from '@heroicons/react/solid';
3-
import { Trans } from '@lingui/macro';
3+
import { Plural, Trans } from '@lingui/macro';
44
import {
55
Box,
66
Collapse,
@@ -20,6 +20,7 @@ import { Row } from 'src/components/primitives/Row';
2020
import { Warning } from 'src/components/primitives/Warning';
2121
import { EmodeCategory } from 'src/helpers/types';
2222
import {
23+
ComputedReserveData,
2324
ExtendedFormattedUser,
2425
useAppDataContext,
2526
} from 'src/hooks/app-data-provider/useAppDataProvider';
@@ -46,22 +47,65 @@ export enum ErrorType {
4647
}
4748

4849
export type EModeCategoryDisplay = EmodeCategory & {
49-
available: boolean; // indicates if the user can enter this category
50+
available: boolean;
51+
blockReason: EModeCategoryBlockReason;
5052
};
5153

52-
// An E-Mode category is available if the user has no borrow positions outside of the category
53-
function isEModeCategoryAvailable(user: ExtendedFormattedUser, eMode: EmodeCategory): boolean {
54-
const borrowableReserves = eMode.assets
55-
.filter((asset) => asset.borrowable)
56-
.map((asset) => asset.underlyingAsset);
54+
export type EModeCategoryBlockReason = {
55+
incompatibleBorrows: string[];
56+
zeroLtvCollateral: string[];
57+
};
5758

58-
const hasIncompatiblePositions = user.userReservesData.some(
59-
(userReserve) =>
60-
Number(userReserve.scaledVariableDebt) > 0 &&
61-
!borrowableReserves.includes(userReserve.reserve.underlyingAsset)
59+
// Checks why an E-Mode category is unavailable for the user
60+
function getEModeCategoryBlockReason(
61+
user: ExtendedFormattedUser,
62+
eMode: EmodeCategory,
63+
reservesByAddress: Map<string, ComputedReserveData>
64+
): EModeCategoryBlockReason {
65+
const incompatibleBorrows: string[] = [];
66+
const zeroLtvCollateral: string[] = [];
67+
68+
// Check 1: Incompatible borrows
69+
const borrowableReserves = new Set(
70+
eMode.assets.filter((asset) => asset.borrowable).map((asset) => asset.underlyingAsset)
6271
);
6372

64-
return !hasIncompatiblePositions;
73+
for (const userReserve of user.userReservesData) {
74+
if (
75+
valueToBigNumber(userReserve.scaledVariableDebt).gt(0) &&
76+
!borrowableReserves.has(userReserve.reserve.underlyingAsset)
77+
) {
78+
incompatibleBorrows.push(userReserve.reserve.symbol);
79+
}
80+
}
81+
82+
// Check 2: Collateral with 0 LTV in target category
83+
for (const userReserve of user.userReservesData) {
84+
if (!userReserve.usageAsCollateralEnabledOnUser) continue;
85+
86+
const reserve = reservesByAddress.get(userReserve.reserve.underlyingAsset);
87+
if (!reserve) continue;
88+
89+
const reserveTargetEmode = reserve.eModes.find((e) => e.id === eMode.id);
90+
91+
if (
92+
reserveTargetEmode &&
93+
reserveTargetEmode.collateralEnabled &&
94+
reserveTargetEmode.ltvzeroEnabled
95+
) {
96+
zeroLtvCollateral.push(reserve.symbol);
97+
} else if (!reserveTargetEmode || !reserveTargetEmode.collateralEnabled) {
98+
if (Number(reserve.baseLTVasCollateral) === 0) {
99+
zeroLtvCollateral.push(reserve.symbol);
100+
}
101+
}
102+
}
103+
104+
return { incompatibleBorrows, zeroLtvCollateral };
105+
}
106+
107+
function isEModeCategoryAvailable(blockReason: EModeCategoryBlockReason): boolean {
108+
return blockReason.incompatibleBorrows.length === 0 && blockReason.zeroLtvCollateral.length === 0;
65109
}
66110

67111
export const EmodeModalContent = ({ user }: { user: ExtendedFormattedUser }) => {
@@ -80,14 +124,20 @@ export const EmodeModalContent = ({ user }: { user: ExtendedFormattedUser }) =>
80124
const { gasLimit, mainTxState: emodeTxState, txError } = useModalContext();
81125
const [disableEmode, setDisableEmode] = useState(false);
82126

127+
const reservesByAddress = new Map(reserves.map((r) => [r.underlyingAsset, r]));
128+
83129
const eModeCategories: Record<number, EModeCategoryDisplay> = Object.fromEntries(
84-
Object.entries(eModes).map(([key, value]) => [
85-
key,
86-
{
87-
...value,
88-
available: isEModeCategoryAvailable(user, value),
89-
},
90-
])
130+
Object.entries(eModes).map(([key, value]) => {
131+
const blockReason = getEModeCategoryBlockReason(user, value, reservesByAddress);
132+
return [
133+
key,
134+
{
135+
...value,
136+
available: isEModeCategoryAvailable(blockReason),
137+
blockReason,
138+
},
139+
];
140+
})
91141
);
92142

93143
// For Horizon markets, use the next available category after [1]
@@ -136,22 +186,22 @@ export const EmodeModalContent = ({ user }: { user: ExtendedFormattedUser }) =>
136186
const zeroLtvCollateralSymbols = user.userReservesData
137187
.filter(
138188
(userReserve) =>
139-
Number(userReserve.scaledATokenBalance) > 0 &&
189+
valueToBigNumber(userReserve.scaledATokenBalance).gt(0) &&
140190
userReserve.reserve.baseLTVasCollateral === '0' &&
141-
userReserve.usageAsCollateralEnabledOnUser &&
142-
userReserve.reserve.reserveLiquidationThreshold !== '0'
191+
userReserve.usageAsCollateralEnabledOnUser
143192
)
144193
.map((r) => r.reserve.symbol);
145194

146195
// error handling
147196
let blockingError: ErrorType | undefined = undefined;
148-
// if user is disabling eMode
149197
if (user.isInEmode && disableEmode) {
150198
if (zeroLtvCollateralSymbols.length > 0) {
151199
blockingError = ErrorType.ZERO_LTV_COLLATERAL_BLOCKING;
152200
} else if (Number(newSummary.healthFactor) < 1.01 && newSummary.healthFactor !== '-1') {
153201
blockingError = ErrorType.EMODE_DISABLED_LIQUIDATION;
154202
}
203+
} else if (!disableEmode && !selectedEmode.available) {
204+
blockingError = ErrorType.CLOSE_POSITIONS_BEFORE_SWITCHING;
155205
}
156206

157207
const Blocked: React.FC = () => {
@@ -184,6 +234,33 @@ export const EmodeModalContent = ({ user }: { user: ExtendedFormattedUser }) =>
184234
</Typography>
185235
</Warning>
186236
);
237+
case ErrorType.CLOSE_POSITIONS_BEFORE_SWITCHING: {
238+
const { incompatibleBorrows, zeroLtvCollateral } = selectedEmode.blockReason;
239+
return (
240+
<Warning severity="info" sx={{ mt: 6, alignItems: 'center' }}>
241+
<Typography variant="subheader1">
242+
<Trans>Cannot switch to this category</Trans>
243+
</Typography>
244+
{incompatibleBorrows.length > 0 && (
245+
<Typography variant="caption">
246+
<Trans>
247+
Repay your {incompatibleBorrows.join(', ')}{' '}
248+
<Plural value={incompatibleBorrows.length} one="borrow" other="borrows" /> to use
249+
this category.
250+
</Trans>
251+
</Typography>
252+
)}
253+
{zeroLtvCollateral.length > 0 && (
254+
<Typography variant="caption">
255+
<Trans>
256+
Disable {zeroLtvCollateral.join(', ')} as collateral to use this category. These
257+
assets would have 0% LTV.
258+
</Trans>
259+
</Typography>
260+
)}
261+
</Warning>
262+
);
263+
}
187264
default:
188265
return null;
189266
}
@@ -390,14 +467,6 @@ export const EmodeModalContent = ({ user }: { user: ExtendedFormattedUser }) =>
390467
))}
391468
</Select>
392469
</Stack>
393-
{!selectedEmode.available && (
394-
<Typography variant="caption" color="text.secondary" sx={{ mb: 3 }}>
395-
<Trans>
396-
All borrow positions outside of this category must be closed to enable this
397-
category.
398-
</Trans>
399-
</Typography>
400-
)}
401470
<Divider />
402471
<Row
403472
captionVariant="description"

src/components/transactions/Supply/CollateralOptionsSelector.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ function getBlockReason(
7676
if (incompatibleBorrow) {
7777
return (
7878
<Trans>
79-
Active {incompatibleBorrow.reserve.symbol} borrow is not compatible with this category
79+
Active {incompatibleBorrow.reserve.symbol} borrow is not compatible with this category.
80+
Repay your {incompatibleBorrow.reserve.symbol} borrow to use this option.
8081
</Trans>
8182
);
8283
}
@@ -93,7 +94,12 @@ function getBlockReason(
9394
if (targetEmodeId === 0) {
9495
// Switching to no e-mode: check base LTV
9596
if (Number(reserve.baseLTVasCollateral) === 0) {
96-
return <Trans>{reserve.symbol} collateral would have 0% LTV without E-Mode</Trans>;
97+
return (
98+
<Trans>
99+
{reserve.symbol} collateral would have 0% LTV without E-Mode. Disable {reserve.symbol}{' '}
100+
as collateral to use this option.
101+
</Trans>
102+
);
97103
}
98104
} else {
99105
// Switching to an e-mode: check if asset is in collateral bitmap with ltvzero
@@ -103,12 +109,22 @@ function getBlockReason(
103109
reserveTargetEmode.collateralEnabled &&
104110
reserveTargetEmode.ltvzeroEnabled
105111
) {
106-
return <Trans>{reserve.symbol} collateral would have 0% LTV in this category</Trans>;
112+
return (
113+
<Trans>
114+
{reserve.symbol} collateral would have 0% LTV in this category. Disable {reserve.symbol}{' '}
115+
as collateral to use this option.
116+
</Trans>
117+
);
107118
}
108119
// If asset is NOT in the category at all, it falls back to base LTV — check that too
109120
if (!reserveTargetEmode || !reserveTargetEmode.collateralEnabled) {
110121
if (Number(reserve.baseLTVasCollateral) === 0) {
111-
return <Trans>{reserve.symbol} collateral would have 0% LTV in this category</Trans>;
122+
return (
123+
<Trans>
124+
{reserve.symbol} collateral would have 0% LTV in this category. Disable{' '}
125+
{reserve.symbol} as collateral to use this option.
126+
</Trans>
127+
);
112128
}
113129
}
114130
}

src/components/transactions/Supply/SupplyModalContent.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -261,20 +261,25 @@ export const SupplyModalContent = React.memo(
261261
return calculateHFAfterSupply(user, poolReserve, amountInEth);
262262
})();
263263

264-
// Override collateral type if switching e-modes changes collateral eligibility
264+
// Override collateral type based on the selected e-mode's collateral eligibility,
265+
// since getAssetCollateralType only considers base config and doesn't account for e-mode
265266
const effectiveCollateralType = (() => {
266-
if (!needsEmodeSwitch) return collateralType;
267+
if (!hasEmodeOptions) return collateralType;
267268
const targetEmode = poolReserve.eModes.find((e) => e.id === selectedEmodeId);
268269
if (selectedEmodeId === 0) {
269-
// Switching to no e-mode — use base config
270+
// Default / no e-mode — use base config
270271
return Number(poolReserve.baseLTVasCollateral) > 0 && poolReserve.usageAsCollateralEnabled
271272
? CollateralType.ENABLED
272273
: CollateralType.UNAVAILABLE;
273274
}
274275
if (targetEmode && targetEmode.collateralEnabled && !targetEmode.ltvzeroEnabled) {
275276
return CollateralType.ENABLED;
276277
}
277-
return CollateralType.DISABLED;
278+
if (targetEmode && targetEmode.collateralEnabled && targetEmode.ltvzeroEnabled) {
279+
return CollateralType.DISABLED;
280+
}
281+
// Not in this category's collateral bitmap — fall back to base
282+
return collateralType;
278283
})();
279284

280285
const supplyActionsProps = {

src/locales/el/messages.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/locales/en/messages.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)