Skip to content

Commit 34b4e69

Browse files
feat: update lens and filtering logic to check for inflation protection (#110)
1 parent 070685d commit 34b4e69

File tree

8 files changed

+219
-69
lines changed

8 files changed

+219
-69
lines changed

apps/lite/src/app/dashboard/borrow-subpage.tsx

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { restructure } from "@morpho-org/blue-sdk-viem";
33
import { metaMorphoFactoryAbi } from "@morpho-org/uikit/assets/abis/meta-morpho-factory";
44
import { morphoAbi } from "@morpho-org/uikit/assets/abis/morpho";
55
import useContractEvents from "@morpho-org/uikit/hooks/use-contract-events/use-contract-events";
6-
import { readAccrualVaults, readAccrualVaultsStateOverride } from "@morpho-org/uikit/lens/read-vaults";
6+
import {
7+
marketHasDeadDeposit,
8+
readAccrualVaults,
9+
readAccrualVaultsStateOverride,
10+
} from "@morpho-org/uikit/lens/read-vaults";
711
import { CORE_DEPLOYMENTS, getContractDeploymentInfo } from "@morpho-org/uikit/lib/deployments";
812
import { Token } from "@morpho-org/uikit/lib/utils";
913
import { useMemo } from "react";
@@ -18,7 +22,7 @@ import * as Merkl from "@/hooks/use-merkl-campaigns";
1822
import { useMerklOpportunities } from "@/hooks/use-merkl-opportunities";
1923
import { useTopNCurators } from "@/hooks/use-top-n-curators";
2024
import { type DisplayableCurators, getDisplayableCurators } from "@/lib/curators";
21-
import { CREATE_METAMORPHO_EVENT_OVERRIDES, getDeploylessMode } from "@/lib/overrides";
25+
import { CREATE_METAMORPHO_EVENT_OVERRIDES, getDeploylessMode, getShouldEnforceDeadDeposit } from "@/lib/overrides";
2226
import { getTokenURI } from "@/lib/tokens";
2327

2428
const STALE_TIME = 5 * 60 * 1000;
@@ -35,6 +39,7 @@ export function BorrowSubPage() {
3539

3640
const shouldOverrideCreateMetaMorphoEvents = chainId !== undefined && chainId in CREATE_METAMORPHO_EVENT_OVERRIDES;
3741
const shouldUseDeploylessReads = getDeploylessMode(chainId) === "deployless";
42+
const shouldEnforceDeadDeposit = getShouldEnforceDeadDeposit(chainId);
3843

3944
const [morpho, factory, factoryV1_1] = useMemo(
4045
() => [
@@ -96,15 +101,22 @@ export function BorrowSubPage() {
96101
},
97102
});
98103

99-
const marketIds = useMemo(
100-
() => [
101-
...new Set(
102-
vaultsData?.flatMap((d) => d.allocations.filter((alloc) => alloc.config.enabled).map((alloc) => alloc.id)) ??
103-
[],
104-
),
105-
],
106-
[vaultsData],
107-
);
104+
const marketIds = useMemo(() => {
105+
// Get a flat map of allocations[i].id across all vaults for allocations that meet certain criteria:
106+
// - they are actually enabled as a potential liquidity sink
107+
// - they have a dead deposit (or dead deposit enforcement is disabled)
108+
const filteredAllocationMarketIds = (vaultsData ?? []).flatMap((vd) =>
109+
vd.allocations
110+
.filter((alloc) => {
111+
const isEnabled = alloc.config.enabled;
112+
const isDeadDepositStateValid = !shouldEnforceDeadDeposit || marketHasDeadDeposit(vd, alloc.id);
113+
114+
return isEnabled && isDeadDepositStateValid;
115+
})
116+
.map((alloc) => alloc.id),
117+
);
118+
return [...new Set(filteredAllocationMarketIds)];
119+
}, [shouldEnforceDeadDeposit, vaultsData]);
108120
const markets = useMarkets({ chainId, marketIds, staleTime: STALE_TIME, fetchPrices: true });
109121
const marketsArr = useMemo(() => {
110122
const marketsArr = Object.values(markets).filter(

apps/lite/src/app/dashboard/earn-subpage.tsx

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import {
1010
import { metaMorphoAbi } from "@morpho-org/uikit/assets/abis/meta-morpho";
1111
import { metaMorphoFactoryAbi } from "@morpho-org/uikit/assets/abis/meta-morpho-factory";
1212
import useContractEvents from "@morpho-org/uikit/hooks/use-contract-events/use-contract-events";
13-
import { readAccrualVaults, readAccrualVaultsStateOverride } from "@morpho-org/uikit/lens/read-vaults";
13+
import {
14+
getDeadDepositsBitmap,
15+
readAccrualVaults,
16+
readAccrualVaultsStateOverride,
17+
vaultHasDeadDeposits,
18+
} from "@morpho-org/uikit/lens/read-vaults";
1419
import { CORE_DEPLOYMENTS, getContractDeploymentInfo } from "@morpho-org/uikit/lib/deployments";
1520
import { Token } from "@morpho-org/uikit/lib/utils";
1621
import { useEffect, useMemo } from "react";
@@ -25,7 +30,7 @@ import * as Merkl from "@/hooks/use-merkl-campaigns";
2530
import { useMerklOpportunities } from "@/hooks/use-merkl-opportunities";
2631
import { useTopNCurators } from "@/hooks/use-top-n-curators";
2732
import { getDisplayableCurators } from "@/lib/curators";
28-
import { CREATE_METAMORPHO_EVENT_OVERRIDES, getDeploylessMode } from "@/lib/overrides";
33+
import { CREATE_METAMORPHO_EVENT_OVERRIDES, getDeploylessMode, getShouldEnforceDeadDeposit } from "@/lib/overrides";
2934
import { getTokenURI } from "@/lib/tokens";
3035

3136
const STALE_TIME = 5 * 60 * 1000;
@@ -37,6 +42,7 @@ export function EarnSubPage() {
3742

3843
const shouldOverrideCreateMetaMorphoEvents = chainId !== undefined && chainId in CREATE_METAMORPHO_EVENT_OVERRIDES;
3944
const shouldUseDeploylessReads = getDeploylessMode(chainId) === "deployless";
45+
const shouldEnforceDeadDeposit = getShouldEnforceDeadDeposit(chainId);
4046

4147
const [morpho, factory, factoryV1_1] = useMemo(
4248
() => [
@@ -116,8 +122,9 @@ export function EarnSubPage() {
116122

117123
const marketIds = useMemo(() => [...new Set(vaultsData?.flatMap((d) => d.vault.withdrawQueue) ?? [])], [vaultsData]);
118124
const markets = useMarkets({ chainId, marketIds, staleTime: STALE_TIME });
119-
const vaults = useMemo(() => {
125+
const { vaults, hasDeadDeposits } = useMemo(() => {
120126
const vaults: AccrualVault[] = [];
127+
const hasDeadDeposits = new Map<Address, boolean>();
121128
vaultsData?.forEach((vaultData) => {
122129
const { vault: address, supplyQueue, withdrawQueue, ...iVault } = vaultData.vault;
123130
// NOTE: pending values are placeholders
@@ -131,13 +138,17 @@ export function EarnSubPage() {
131138
pendingTimelock: { value: 0n, validAt: 0n },
132139
});
133140

134-
if (vault.name === "" || vaultData.allocations.some((allocation) => markets[allocation.id] === undefined)) {
141+
const shouldSkipVault =
142+
vault.name === "" || vaultData.allocations.some((allocation) => markets[allocation.id] === undefined);
143+
144+
if (shouldSkipVault) {
135145
const urlSearchParams = new URLSearchParams(window.location.search);
136146
if (urlSearchParams.has("dev")) {
137147
// Detailed logging of filtering reason to help curators diagnose their situation.
138148
console.log(`Skipping vault '${vault.name}':
139149
- ${vault.name === "" ? "❌" : "✅"} name is defined
140150
- ${vaultData.allocations.some((allocation) => markets[allocation.id] === undefined) ? "❌" : "✅"} fetched constituent markets
151+
- ${shouldEnforceDeadDeposit && !hasDeadDeposits ? "❌" : "✅"} has dead deposits (${getDeadDepositsBitmap(vaultData)})
141152
`);
142153
}
143154
return;
@@ -167,10 +178,11 @@ export function EarnSubPage() {
167178
});
168179

169180
vaults.push(new AccrualVault(vault, allocations));
181+
hasDeadDeposits.set(vault.address, vaultHasDeadDeposits(vaultData));
170182
});
171183
vaults.sort((a, b) => (a.netApy > b.netApy ? -1 : 1));
172-
return vaults;
173-
}, [vaultsData, markets]);
184+
return { vaults, hasDeadDeposits };
185+
}, [shouldEnforceDeadDeposit, vaultsData, markets]);
174186

175187
// MARK: Fetch metadata for every ERC20 asset
176188
const tokenAddresses = useMemo(() => {
@@ -230,23 +242,27 @@ export function EarnSubPage() {
230242
) as { [vault: Address]: bigint | undefined };
231243

232244
const rows = useMemo(() => {
233-
return vaults.map((vault) => {
234-
const { decimals, symbol } = tokens.get(vault.asset) ?? { decimals: undefined, symbol: undefined };
245+
return vaults
246+
.map((vault) => {
247+
const { decimals, symbol } = tokens.get(vault.asset) ?? { decimals: undefined, symbol: undefined };
248+
const isDeadDepositStateValid = !shouldEnforceDeadDeposit || (hasDeadDeposits.get(vault.address) ?? false);
235249

236-
return {
237-
vault,
238-
asset: {
239-
address: vault.asset,
250+
return {
251+
vault,
252+
isDeadDepositStateValid,
253+
asset: {
254+
address: vault.asset,
255+
imageSrc: getTokenURI({ symbol, address: vault.asset, chainId }),
256+
symbol,
257+
decimals,
258+
} as Token,
259+
curators: getDisplayableCurators(vault, curators, chainId),
260+
userShares: userShares[vault.address],
240261
imageSrc: getTokenURI({ symbol, address: vault.asset, chainId }),
241-
symbol,
242-
decimals,
243-
} as Token,
244-
curators: getDisplayableCurators(vault, curators, chainId),
245-
userShares: userShares[vault.address],
246-
imageSrc: getTokenURI({ symbol, address: vault.asset, chainId }),
247-
};
248-
});
249-
}, [vaults, tokens, userShares, curators, chainId]);
262+
};
263+
})
264+
.filter((vault) => vault.isDeadDepositStateValid || (vault.userShares ?? 0n) > 0n);
265+
}, [vaults, hasDeadDeposits, shouldEnforceDeadDeposit, tokens, userShares, curators, chainId]);
250266

251267
const userRows = rows.filter((row) => (row.userShares ?? 0n) > 0n);
252268

apps/lite/src/components/earn-sheet-content.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,15 @@ const STYLE_INPUT_WRAPPER =
3030
"bg-primary hover:bg-secondary flex flex-col gap-4 rounded-2xl p-4 transition-colors duration-200 ease-in-out";
3131
const STYLE_INPUT_HEADER = "text-secondary-foreground flex items-center justify-between text-xs font-light";
3232

33-
export function EarnSheetContent({ vaultAddress, asset }: { vaultAddress: Address; asset: Token }) {
33+
export function EarnSheetContent({
34+
vaultAddress,
35+
isDeadDepositStateValid,
36+
asset,
37+
}: {
38+
vaultAddress: Address;
39+
isDeadDepositStateValid: boolean;
40+
asset: Token;
41+
}) {
3442
const { address: userAddress } = useAccount();
3543

3644
const [selectedTab, setSelectedTab] = useState(Actions.Deposit);
@@ -41,7 +49,7 @@ export function EarnSheetContent({ vaultAddress, asset }: { vaultAddress: Addres
4149
address: userAddress,
4250
query: { enabled: !!userAddress },
4351
});
44-
const canInteract = userBytecode !== undefined && userBytecode === null;
52+
const isProtectedFromInflation = isDeadDepositStateValid || (userBytecode !== undefined && userBytecode === null);
4553

4654
const { data: allowance, refetch: refetchAllowance } = useReadContract({
4755
address: asset.address,
@@ -157,15 +165,15 @@ export function EarnSheetContent({ vaultAddress, asset }: { vaultAddress: Addres
157165
{approvalTxnConfig ? (
158166
<TransactionButton
159167
variables={approvalTxnConfig}
160-
disabled={inputValue === 0n || !canInteract}
168+
disabled={inputValue === 0n || !isProtectedFromInflation}
161169
onTxnReceipt={() => refetchAllowance()}
162170
>
163171
Approve
164172
</TransactionButton>
165173
) : (
166174
<TransactionButton
167175
variables={depositTxnConfig}
168-
disabled={!inputValue || !canInteract}
176+
disabled={!inputValue || !isProtectedFromInflation}
169177
onTxnReceipt={() => {
170178
setTextInputValue("");
171179
void refetchMaxes();

apps/lite/src/components/earn-table.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { getTokenURI } from "@/lib/tokens";
2929

3030
export type Row = {
3131
vault: AccrualVault;
32+
isDeadDepositStateValid: boolean;
3233
asset: Token;
3334
curators: DisplayableCurators;
3435
userShares: bigint | undefined;
@@ -326,7 +327,11 @@ export function EarnTable({
326327
</TableCell>
327328
</TableRow>
328329
</SheetTrigger>
329-
<EarnSheetContent vaultAddress={row.vault.address} asset={row.asset} />
330+
<EarnSheetContent
331+
vaultAddress={row.vault.address}
332+
isDeadDepositStateValid={row.isDeadDepositStateValid}
333+
asset={row.asset}
334+
/>
330335
</Sheet>
331336
);
332337
})}

apps/lite/src/lib/overrides.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { tac } from "@morpho-org/uikit/lib/chains/tac";
2+
import { CORE_DEPLOYMENTS } from "@morpho-org/uikit/lib/deployments";
23
import { type Address } from "viem";
3-
import { sei } from "viem/chains";
4+
import { celo, sei } from "viem/chains";
45

56
export const CREATE_METAMORPHO_EVENT_OVERRIDES: Record<number, Address[]> = {
67
[sei.id]: ["0x015F10a56e97e02437D294815D8e079e1903E41C", "0x948FcC6b7f68f4830Cd69dB1481a9e1A142A4923"],
@@ -17,3 +18,12 @@ export function getDeploylessMode(chainId: number | undefined): "deployless" | "
1718
if (chainId === undefined) return "stateOverride";
1819
return DEPLOYLESS_MODE_OVERRIDES[chainId] ?? "stateOverride";
1920
}
21+
22+
// On these chains, vaults/markets must have >= 1e9 shares owned by the 0xDEAD address in order
23+
// to show up, and contract accounts are allowed to deposit. On all other chains, the 0xDEAD
24+
// requirement is unenforced, and contract accounts are blocked from depositing.
25+
const ENFORCE_DEAD_DEPOSIT_CHAINS = [...CORE_DEPLOYMENTS, celo.id];
26+
27+
export function getShouldEnforceDeadDeposit(chainId: number | undefined) {
28+
return chainId !== undefined && ENFORCE_DEAD_DEPOSIT_CHAINS.includes(chainId);
29+
}

apps/lite/test/components/earn-sheet-content.test.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ describe("deposit flow", () => {
7272
await client.deal({ account, amount: parseEther(amount), erc20: asset.address }); // for deposit
7373
await client.impersonateAccount({ address: account });
7474

75-
render(<TestableEarnSheetContent vaultAddress={vaultAddress} asset={asset} />, { wagmiConfig });
75+
render(<TestableEarnSheetContent vaultAddress={vaultAddress} asset={asset} isDeadDepositStateValid={true} />, {
76+
wagmiConfig,
77+
});
7678

7779
// Wait for tabs -- this implies the `Testable` wrapper has connected the mock account
7880
await waitFor(() => screen.findAllByRole("tab"));
@@ -176,7 +178,9 @@ describe("withdraw flow", () => {
176178
]);
177179
const maxText = formatUnits(maxWithdraw, asset.decimals!);
178180

179-
render(<TestableEarnSheetContent vaultAddress={vaultAddress} asset={asset} />, { wagmiConfig });
181+
render(<TestableEarnSheetContent vaultAddress={vaultAddress} asset={asset} isDeadDepositStateValid={true} />, {
182+
wagmiConfig,
183+
});
180184

181185
// Wait for tabs -- this implies the `Testable` wrapper has connected the mock account
182186
await waitFor(() => screen.findAllByRole("tab"));
@@ -247,7 +251,9 @@ describe("withdraw flow", () => {
247251
await client.deal({ account, amount: parseEther(shares), erc20: vaultAddress }); // for withdraw
248252
await client.impersonateAccount({ address: account });
249253

250-
render(<TestableEarnSheetContent vaultAddress={vaultAddress} asset={asset} />, { wagmiConfig });
254+
render(<TestableEarnSheetContent vaultAddress={vaultAddress} asset={asset} isDeadDepositStateValid={true} />, {
255+
wagmiConfig,
256+
});
251257

252258
// Wait for tabs -- this implies the `Testable` wrapper has connected the mock account
253259
await waitFor(() => screen.findAllByRole("tab"));

0 commit comments

Comments
 (0)