DocsComponents
Loop
One-click loop on mcUSDC / USDC: supply USDC, borrow against the mcUSDC and supply that too, in one transaction through Morpho's Bundler3 — leverage presets, the net rate, how far mcUSDC can fall before liquidation, and closing the loop back to USDC. The Earn block uses it for the Boosted route.
"use client";
// Mercato kit — one-click loop: supply USDC, borrow against the mcUSDC and supply again, in one transaction.
// https://mercato.sh/developers · Pass your own wallet button as `connect`.
import { useEffect, useState, type ReactNode } from "react";
import { cn } from "@/lib/utils";
import { MERCATO_DISCLOSURE, fmtPct, fmtUsd, loopCushion, loopRate, useMercato, useMercatoLoop } from "@/lib/mercato";
/** `amount` (optional) controls the amount from outside — e.g. the Earn block — and hides this block's own input. */
export function MercatoLoop({ api, connect, defaultAmount = "1000", amount: controlled, className }: { api?: string; connect?: ReactNode; defaultAmount?: string; amount?: string; className?: string }) {
const { data, isLoading } = useMercato(api);
const l = useMercatoLoop(data);
const loop = data?.loop;
const presets = loop?.presets ?? [];
const [own, setAmount] = useState(defaultAmount);
const amount = controlled ?? own;
const [leverage, setLeverage] = useState<number>();
const [failed, setFailed] = useState<string | null>(null);
useEffect(() => { if (leverage == null && presets.length) setLeverage(presets[Math.min(1, presets.length - 1)]); }, [leverage, presets]);
const n = Number(amount) || 0;
const lev = leverage ?? 1;
const borrow = n * (lev - 1);
const rate = loopRate(data?.vault.rate, loop?.borrowRate, lev);
const short = l.walletUsdc != null && n > l.walletUsdc;
const noRoom = loop != null && borrow > loop.liquidityUsd;
const run = async (f: () => Promise<void>) => {
setFailed(null);
try { await f(); } catch (e) { setFailed(e instanceof Error && /reject|denied/i.test(e.message) ? "Cancelled in your wallet." : "Something went wrong — nothing moved unless your wallet shows it. Try again."); }
};
if (isLoading) return <div className={cn("h-96 animate-pulse rounded-xl bg-secondary", className)} />;
if (!loop) return null;
const pos = l.position;
return (
<div className={cn("flex flex-col gap-4 rounded-xl border border-border bg-card p-5 text-card-foreground", className)}>
<div>
<div className="font-medium">Boost with a loop</div>
<p className="mt-1 text-sm text-muted-foreground">Supply USDC, borrow against your mcUSDC and supply that too — in one transaction. More of your money earns, and it can be liquidated if mcUSDC's price falls.</p>
</div>
{pos && (
<div className="flex flex-col gap-3 rounded-lg bg-secondary/60 p-3 text-sm">
<div className="grid grid-cols-3 gap-2">
<div><div className="text-xs text-muted-foreground">Yours</div><div className="mercato-figure tabular-nums">{fmtUsd(pos.equityUsd, 2)}</div></div>
<div><div className="text-xs text-muted-foreground">Leverage</div><div className="mercato-figure tabular-nums">{pos.leverage ? `${pos.leverage.toFixed(2)}×` : "—"}</div></div>
<div><div className="text-xs text-muted-foreground">Borrowed</div><div className="mercato-figure tabular-nums">{fmtUsd(pos.debtUsd, 2)}</div></div>
</div>
{pos.leverage && <div className="text-xs text-muted-foreground">Can be liquidated if mcUSDC falls {fmtPct(loopCushion(pos.leverage, loop.lltv), 1)} from here.</div>}
<button type="button" disabled={l.isPending} onClick={() => run(l.close)}
className="h-10 rounded-lg border border-border font-medium transition hover:bg-secondary disabled:opacity-40">{l.step === "Closing…" ? "Closing…" : "Close loop — get USDC back"}</button>
</div>
)}
{controlled == null && <div className="flex flex-col gap-1.5">
<label className="flex items-center gap-2 rounded-lg border border-border bg-background/40 px-3 py-2.5">
<span className="text-2xl text-muted-foreground">$</span>
<input inputMode="decimal" value={amount} onChange={(e) => { setAmount(e.target.value.replace(/[^0-9.]/g, "")); setFailed(null); }}
className="mercato-figure w-full min-w-0 bg-transparent text-2xl tabular-nums outline-none" placeholder="0" aria-label="Amount in USDC" />
<span className="text-sm text-muted-foreground">USDC</span>
</label>
{l.walletUsdc != null && <div className="text-xs text-muted-foreground">In your wallet: <span className="mercato-figure tabular-nums">{fmtUsd(l.walletUsdc, 2)}</span></div>}
</div>}
<div role="radiogroup" aria-label="Leverage" className="grid gap-1.5" style={{ gridTemplateColumns: `repeat(${presets.length}, minmax(0, 1fr))` }}>
{presets.map((p) => (
<button key={p} type="button" role="radio" aria-checked={p === leverage} onClick={() => setLeverage(p)}
className={cn("flex flex-col items-center rounded-lg py-2 text-sm transition", p === leverage ? "bg-primary text-primary-foreground" : "bg-secondary text-secondary-foreground hover:bg-secondary/80")}>
<span className="font-medium">{p}×</span>
<span className={cn("text-xs", p === leverage ? "opacity-80" : "text-muted-foreground")}>{fmtPct(loopRate(data?.vault.rate, loop.borrowRate, p), 1)}</span>
</button>
))}
</div>
<dl className="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
<dt className="text-muted-foreground">Earning on</dt><dd className="mercato-figure text-right tabular-nums">{fmtUsd(n * lev, 2)}</dd>
<dt className="text-muted-foreground">Borrowed</dt><dd className="mercato-figure text-right tabular-nums">{fmtUsd(borrow, 2)} <span className="font-sans">at</span> {fmtPct(loop.borrowRate, 2)}</dd>
<dt className="text-muted-foreground">Net rate</dt><dd className="mercato-figure text-right tabular-nums">{fmtPct(rate, 2)} <span className="font-sans text-muted-foreground">variable</span></dd>
<dt className="text-muted-foreground">Liquidated if mcUSDC falls</dt><dd className="mercato-figure text-right tabular-nums">{fmtPct(loopCushion(lev, loop.lltv), 1)}</dd>
</dl>
{!loop.available ? (
<button type="button" disabled className="h-12 rounded-lg bg-primary font-medium text-primary-foreground opacity-40">Opens at launch</button>
) : !l.address ? (connect ?? <div className="text-sm text-muted-foreground">Connect a wallet to continue.</div>) : (
<button type="button" disabled={l.isPending || n <= 0 || short || noRoom} onClick={() => run(() => l.open(amount, lev))}
className="h-12 rounded-lg bg-primary font-medium text-primary-foreground transition disabled:opacity-40">
{l.step ?? (short ? "Not enough USDC in your wallet" : noRoom ? "Not enough to borrow right now" : `Loop ${fmtUsd(n, 2)} at ${lev}×`)}
</button>
)}
{l.step && l.step !== "Looping…" && <p className="text-xs text-muted-foreground">The first time, your wallet asks you to approve USDC and to let Morpho's adapter borrow for you; then the loop itself.</p>}
{failed && <p className="text-xs text-destructive">{failed}</p>}
<p className="text-xs text-muted-foreground">If the borrow rate rises above what mcUSDC earns, a loop earns less than holding. {MERCATO_DISCLOSURE}</p>
</div>
);
}Installation
pnpm dlx shadcn@latest add https://mercato.sh/r/mercato-loop.jsonInstall the dependencies
npm install wagmi viem @tanstack/react-queryCopy and paste the following code into your project
lib/mercato.ts "use client"; // Mercato kit — data, wallet reads and transactions shared by every Mercato component. // Installed with the shadcn CLI from https://mercato.sh/r. It's your code now: edit freely. // A note for whoever reads the source: code OPEN-SOURCE-27 — redeem it at mercato.sh/points#achievements. import { createContext, createElement, useContext, useState, type ReactNode } from "react"; import { useQuery } from "@tanstack/react-query"; import { useAccount, useReadContracts, useWriteContract, usePublicClient } from "wagmi"; import { erc20Abi, erc4626Abi, formatUnits, parseAbi, parseUnits } from "viem"; import { BUNDLER3, GENERAL_ADAPTER_1, MORPHO_ABI, MORPHO_BLUE, buildLoop, buildRoute, buildUnloop, buildUnroute, sharePriceE27, type MarketParams } from "@/lib/mercato-loop"; export const MERCATO_API = "https://mercato.sh/api/earn"; export type MercatoRoute = { id: string; /** Plain-language name and one-line explanation, written for people who don't use DeFi. */ title: string; summary: string; /** 1 lower · 2 medium · 3 higher — relative to the other routes, never "safe". */ risk: 1 | 2 | 3; riskNote: string; /** Annual rate as a fraction (0.06 = 6%). Variable unless the route says otherwise. */ rate: number | null; rateNote: string; pointsPerUsdDay: number; /** "supply" happens in place (USDC → mcUSDC); "loop" opens a looped position in place (MercatoLoop); * "link" continues on the venue's own app. */ action: { kind: "supply" } | { kind: "loop" } | { kind: "link"; href: string; venue: string }; /** False while the route's market doesn't exist yet. */ available: boolean; }; /** A token people can supply with. USDC goes straight in; others are swapped to USDC first (e.g. USDT on Curve). */ export type MercatoInput = { symbol: string; address: `0x${string}`; decimals: number; swap?: { venue: string; pool: `0x${string}`; i: number; j: number; maxSlippage: number }; }; export type MercatoData = { vault: { address: `0x${string}`; asset: `0x${string}`; chainId: number; sharePrice: number; rate: number | null; rateNote: string; withdrawableUsd: number; supplyOpen: boolean; inputs: MercatoInput[]; }; routes: MercatoRoute[]; /** Looping on the mcUSDC / USDC market: supply mcUSDC as collateral, borrow USDC, supply it again — in one * transaction through Morpho's Bundler3. Absent where the API doesn't offer it. */ loop?: MercatoLoopData; /** Collateral routes — put in a token you hold (AVT), borrow USDC against it, hold the USDC as mcUSDC. */ collateralRoutes?: MercatoCollateralRouteData[]; updatedAt: string; }; export type MercatoLoopData = { available: boolean; marketId: `0x${string}`; /** The market's immutable parameters; `lltv` as a decimal string (1e18 = 100%). */ market: { loanToken: `0x${string}`; collateralToken: `0x${string}`; oracle: `0x${string}`; irm: `0x${string}`; lltv: string }; lltv: number; /** The highest LTV offered when opening, below `lltv` for a safety margin. */ maxLtv: number; /** Leverage presets (2 = twice your deposit earning). */ presets: number[]; borrowRate: number | null; /** What lenders of USDC into the market earn, and how much of it is borrowed. */ supplyRate?: number | null; utilization?: number | null; /** USDC the market can still lend. */ liquidityUsd: number; }; export type MercatoCollateralRouteData = { id: string; /** "AVT → mcUSDC": what you put in → what you hold. */ label: string; available: boolean; collateral: { symbol: string; address: `0x${string}`; decimals: number; logo?: string | null }; /** Display price in USD and where it is from; the market itself prices the collateral with its oracle. */ priceUsd: number | null; priceSource: string; /** Null until the market exists. */ marketId: `0x${string}` | null; market: MercatoLoopData["market"] | null; lltv: number; /** LTVs offered when opening (0.2 = borrow 20% of the collateral's value). */ ltvPresets: number[]; borrowRate: number | null; /** Rewards paid on the borrowed dollars (e.g. AVT from Aventus nodes), shown on their own line. */ rewards: { symbol: string; apr: number; paidBy: string } | null; liquidityUsd: number; }; const ApiContext = createContext(MERCATO_API); /** Optional: set the API once for every Mercato component below it (e.g. your own proxy). */ export function MercatoProvider({ api = MERCATO_API, children }: { api?: string; children: ReactNode }) { return createElement(ApiContext.Provider, { value: api }, children); } /** Live vault figures and the preset routes. `api` overrides the provider for one component. */ export function useMercato(api?: string) { const fromProvider = useContext(ApiContext); const url = api ?? fromProvider; return useQuery({ queryKey: ["mercato", url], queryFn: async (): Promise<MercatoData> => { const r = await fetch(url); if (!r.ok) throw new Error(`Mercato API ${r.status}`); return r.json(); }, staleTime: 60_000, }); } const curveAbi = parseAbi([ "function get_dy(int128 i, int128 j, uint256 dx) view returns (uint256)", "function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy)", ]); /** USDT-style approve: no return value, and a non-zero allowance must be reset to zero first. */ const approveAbi = parseAbi(["function approve(address spender, uint256 amount)"]); // BigInt(…) rather than ZERO literals, so the file compiles in projects targeting ES2017 (create-next-app's default). const ZERO = BigInt(0); /** The connected wallet: balance of each input token, its mcUSDC, and what that mcUSDC is worth in USDC. */ export function useMercatoPosition(vault?: MercatoData["vault"]) { const { address } = useAccount(); const on = !!address && !!vault; const inputs = vault?.inputs ?? []; const { data, refetch } = useReadContracts({ allowFailure: true, contracts: on ? [ { address: vault.address, abi: erc20Abi, functionName: "balanceOf", args: [address] } as const, // mcUSDC is an ERC-20 too ...inputs.map((t) => ({ address: t.address, abi: erc20Abi, functionName: "balanceOf", args: [address] }) as const), ...inputs.map((t) => ({ address: t.address, abi: erc20Abi, functionName: "allowance", args: [address, t.swap?.pool ?? vault.address] }) as const), ] : [], }); const shares = data?.[0]?.result as bigint | undefined; const balances = Object.fromEntries(inputs.map((t, k) => [t.symbol, data?.[1 + k]?.result as bigint | undefined])); const allowances = Object.fromEntries(inputs.map((t, k) => [t.symbol, data?.[1 + inputs.length + k]?.result as bigint | undefined])); const { data: value } = useReadContracts({ allowFailure: true, contracts: on && shares != null ? [{ address: vault.address, abi: erc4626Abi, functionName: "convertToAssets", args: [shares] }] : [], }); const assets = value?.[0]?.result as bigint | undefined; const tokenBalance = (symbol: string) => { const t = inputs.find((x) => x.symbol === symbol), b = balances[symbol]; return t && b != null ? Number(formatUnits(b, t.decimals)) : null; }; return { address, shares, /** What the wallet's mcUSDC is worth now, in USDC. */ balance: assets != null ? Number(formatUnits(assets, 6)) : shares === ZERO ? 0 : null, usdc: tokenBalance("USDC"), tokenBalance, allowances, refetch, }; } /** How much USDC an amount of `symbol` becomes before it is supplied — 1:1 for USDC, the pool's own quote otherwise. */ export function useMercatoQuote(vault: MercatoData["vault"] | undefined, symbol: string, amount: string) { const t = vault?.inputs.find((x) => x.symbol === symbol); let dx = ZERO; try { dx = t ? parseUnits(amount || "0", t.decimals) : ZERO; } catch { /* not a number yet */ } const { data } = useReadContracts({ allowFailure: true, contracts: t?.swap && dx > ZERO ? [{ address: t.swap.pool, abi: curveAbi, functionName: "get_dy", args: [BigInt(t.swap.i), BigInt(t.swap.j), dx] }] : [], }); if (!t) return { usdc: null, input: undefined }; if (!t.swap) return { usdc: Number(amount) || 0, input: t }; const dy = data?.[0]?.result as bigint | undefined; return { usdc: dy != null ? Number(formatUnits(dy, 6)) : null, input: t }; } /** What the wallet has earned in Mercato so far, in USDC (Morpho's own figure, via the Mercato API). */ export function useMercatoEarned(api?: string) { const fromProvider = useContext(ApiContext); const url = api ?? fromProvider; const { address } = useAccount(); return useQuery({ queryKey: ["mercato-position", url, address], enabled: !!address, queryFn: async (): Promise<{ balance: number; earned: number } | null> => { const r = await fetch(`${url}/position?address=${address}`); if (!r.ok) throw new Error(`Mercato API ${r.status}`); return r.json(); }, staleTime: 60_000, }); } /** Supply (swapping to USDC first if needed) and withdraw. Users sign every step in their own wallet. */ export function useMercatoActions(vault?: MercatoData["vault"]) { const pos = useMercatoPosition(vault); const client = usePublicClient(); const { writeContractAsync } = useWriteContract(); /** What is happening now, for the button ("Swapping USDT…"); null when idle. */ const [step, setStep] = useState<string | null>(null); const wait = async (hash: `0x${string}`) => { const r = await client?.waitForTransactionReceipt({ hash }); if (r?.status === "reverted") throw new Error("Transaction failed"); }; async function approve(token: `0x${string}`, spender: `0x${string}`, amount: bigint, current: bigint | undefined) { if ((current ?? ZERO) >= amount) return; setStep(`Approving ${vault?.inputs.find((x) => x.address === token)?.symbol ?? "token"}…`); if ((current ?? ZERO) > ZERO) await wait(await writeContractAsync({ address: token, abi: approveAbi, functionName: "approve", args: [spender, ZERO] })); await wait(await writeContractAsync({ address: token, abi: approveAbi, functionName: "approve", args: [spender, amount] })); } /** Supply `amount` of `symbol` (default USDC). Other tokens are swapped to USDC on the input's pool, then the USDC received is supplied. */ async function supply(amount: string, symbol = "USDC") { const t = vault?.inputs.find((x) => x.symbol === symbol); const usdc = vault?.inputs.find((x) => x.symbol === "USDC"); if (!vault || !usdc || !pos.address || !t || !client) return; let units = parseUnits(amount || "0", t.decimals); if (units === ZERO) return; setStep("Preparing…"); try { if (t.swap) { const { pool, i, j, venue, maxSlippage } = t.swap; const quote = await client.readContract({ address: pool, abi: curveAbi, functionName: "get_dy", args: [BigInt(i), BigInt(j), units] }); const minOut = (quote * BigInt(Math.round((1 - maxSlippage) * 10_000))) / BigInt(10_000); await approve(t.address, pool, units, pos.allowances[symbol]); const before = await client.readContract({ address: usdc.address, abi: erc20Abi, functionName: "balanceOf", args: [pos.address] }); setStep(`Swapping ${symbol} to USDC on ${venue}…`); await wait(await writeContractAsync({ address: pool, abi: curveAbi, functionName: "exchange", args: [BigInt(i), BigInt(j), units, minOut] })); const after = await client.readContract({ address: usdc.address, abi: erc20Abi, functionName: "balanceOf", args: [pos.address] }); units = after - before; // supply exactly what the swap returned const allowance = await client.readContract({ address: usdc.address, abi: erc20Abi, functionName: "allowance", args: [pos.address, vault.address] }); await approve(usdc.address, vault.address, units, allowance); } else { await approve(t.address, vault.address, units, pos.allowances[symbol]); } setStep("Supplying…"); await wait(await writeContractAsync({ address: vault.address, abi: erc4626Abi, functionName: "deposit", args: [units, pos.address] })); } finally { setStep(null); pos.refetch(); } } /** Withdraw an amount of USDC, or everything (redeems every share). */ async function withdraw(amountUsdc: string | "all") { if (!vault || !pos.address) return; setStep("Withdrawing…"); try { await wait(amountUsdc === "all" ? await writeContractAsync({ address: vault.address, abi: erc4626Abi, functionName: "redeem", args: [pos.shares ?? ZERO, pos.address, pos.address] }) : await writeContractAsync({ address: vault.address, abi: erc4626Abi, functionName: "withdraw", args: [parseUnits(amountUsdc || "0", 6), pos.address, pos.address] })); } finally { setStep(null); pos.refetch(); } } return { ...pos, supply, withdraw, step, isPending: step != null }; } const parseMarket = (m: MercatoLoopData["market"]): MarketParams => ({ ...m, lltv: BigInt(m.lltv) }); /** Net rate of a loop: the vault rate on everything you hold, minus the borrow rate on what you borrowed. */ export const loopRate = (vaultRate: number | null | undefined, borrowRate: number | null | undefined, leverage: number) => vaultRate == null || borrowRate == null ? null : vaultRate * leverage - borrowRate * (leverage - 1); /** How far mcUSDC's price can fall before a loop at this leverage can be liquidated. */ export const loopCushion = (leverage: number, lltv: number) => 1 - (1 - 1 / leverage) / lltv; /** The wallet's looped position, and one-transaction loop / close. Users sign in their own wallet: once to approve * USDC and once to let Morpho's adapter act for them (both only the first time), then the loop itself. */ export function useMercatoLoop(data?: MercatoData) { const { address } = useAccount(); const client = usePublicClient(); const { writeContractAsync } = useWriteContract(); const [step, setStep] = useState<string | null>(null); const loop = data?.loop, vault = data?.vault; const id = loop?.marketId; const reads = useReadContracts({ allowFailure: true, contracts: loop && vault && address ? [ { address: MORPHO_BLUE, abi: MORPHO_ABI, functionName: "position", args: [id!, address] }, { address: MORPHO_BLUE, abi: MORPHO_ABI, functionName: "market", args: [id!] }, { address: MORPHO_BLUE, abi: MORPHO_ABI, functionName: "isAuthorized", args: [address, GENERAL_ADAPTER_1] }, { address: vault.asset, abi: erc20Abi, functionName: "allowance", args: [address, GENERAL_ADAPTER_1] }, { address: loop.market.collateralToken, abi: erc20Abi, functionName: "decimals" }, { address: vault.asset, abi: erc20Abi, functionName: "balanceOf", args: [address] }, ] : [], query: { enabled: !!(loop && vault && address), refetchInterval: 30_000 }, }); const r = reads.data ?? []; const pos = r[0]?.result as readonly [bigint, bigint, bigint] | undefined; const mkt = r[1]?.result as readonly [bigint, bigint, bigint, bigint, bigint, bigint] | undefined; const authorized = r[2]?.result as boolean | undefined; const allowance = r[3]?.result as bigint | undefined; const shareDecimals = (r[4]?.result as number | undefined) ?? 18; const usdc = r[5]?.result as bigint | undefined; const debtUnits = pos && mkt && mkt[3] > ZERO ? (pos[1] * mkt[2] + mkt[3] - BigInt(1)) / mkt[3] : ZERO; const collateralUsd = pos && vault ? Number(formatUnits(pos[2], shareDecimals)) * vault.sharePrice : 0; const debtUsd = Number(formatUnits(debtUnits, 6)); const equity = collateralUsd - debtUsd; const position = collateralUsd > 0 ? { collateralUsd, debtUsd, equityUsd: equity, ltv: debtUsd / collateralUsd, leverage: equity > 0 ? collateralUsd / equity : null } : null; const wait = async (hash: `0x${string}`) => { const rc = await client?.waitForTransactionReceipt({ hash }); if (rc?.status === "reverted") throw new Error("Transaction failed"); }; async function sharePrice() { const one = await client!.readContract({ address: loop!.market.collateralToken, abi: erc4626Abi, functionName: "convertToAssets", args: [BigInt(10) ** BigInt(shareDecimals)] }); return sharePriceE27(one, shareDecimals); } async function authorize() { if (authorized) return; setStep("Allowing Morpho's adapter to borrow for you…"); await wait(await writeContractAsync({ address: MORPHO_BLUE, abi: MORPHO_ABI, functionName: "setAuthorization", args: [GENERAL_ADAPTER_1, true] })); } /** Put `amount` USDC in at `leverage` (2 = twice your deposit earns the vault rate). */ async function open(amount: string, leverage: number) { if (!loop || !vault || !address || !client) return; const deposit = parseUnits(amount || "0", 6); if (deposit === ZERO) return; const borrow = (deposit * BigInt(Math.round((leverage - 1) * 10_000))) / BigInt(10_000); try { if ((allowance ?? ZERO) < deposit) { setStep("Approving USDC…"); await wait(await writeContractAsync({ address: vault.asset, abi: approveAbi, functionName: "approve", args: [GENERAL_ADAPTER_1, deposit] })); } await authorize(); setStep("Looping…"); await wait(await writeContractAsync(buildLoop({ user: address, vault: loop.market.collateralToken, market: parseMarket(loop.market), depositAssets: deposit, borrowAssets: borrow, sharePriceE27: await sharePrice() }))); } finally { setStep(null); reads.refetch(); } } /** Close the whole loop: repay the debt, take the collateral back and receive the rest as USDC. Needs available * liquidity in the vault for the redeem. */ async function close() { if (!loop || !address || !client || debtUnits === ZERO) return; try { await authorize(); setStep("Closing…"); const minPrice = ((await sharePrice()) * BigInt(9_990)) / BigInt(10_000); await wait(await writeContractAsync(buildUnloop({ user: address, vault: loop.market.collateralToken, market: parseMarket(loop.market), debtAssets: (debtUnits * BigInt(10_010)) / BigInt(10_000) + BigInt(1), minSharePriceE27: minPrice }))); } finally { setStep(null); reads.refetch(); } } return { address, position, walletUsdc: usdc == null ? null : Number(formatUnits(usdc, 6)), open, close, step, isPending: step != null, bundler: BUNDLER3 }; } const MP = "(address loanToken, address collateralToken, address oracle, address irm, uint256 lltv)"; const LEND_ABI = parseAbi([ `function supply(${MP} marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) returns (uint256, uint256)`, `function withdraw(${MP} marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) returns (uint256, uint256)`, ]); /** Lend USDC in the mcUSDC / USDC market — to anyone borrowing against mcUSDC — and take it back. Users sign in their * own wallet: approve USDC to Morpho once, then supply. Withdrawing takes what isn't borrowed at the time. */ export function useMercatoLend(data?: MercatoData) { const { address } = useAccount(); const client = usePublicClient(); const { writeContractAsync } = useWriteContract(); const [step, setStep] = useState<string | null>(null); const loop = data?.loop; const usdc = loop?.market.loanToken; const reads = useReadContracts({ allowFailure: true, contracts: loop && usdc && address ? [ { address: usdc, abi: erc20Abi, functionName: "balanceOf", args: [address] }, { address: usdc, abi: erc20Abi, functionName: "allowance", args: [address, MORPHO_BLUE] }, { address: MORPHO_BLUE, abi: MORPHO_ABI, functionName: "position", args: [loop.marketId, address] }, { address: MORPHO_BLUE, abi: MORPHO_ABI, functionName: "market", args: [loop.marketId] }, ] : [], query: { enabled: !!(loop && address), refetchInterval: 30_000 }, }); const r = reads.data ?? []; const wallet = r[0]?.result as bigint | undefined; const allowance = r[1]?.result as bigint | undefined; const pos = r[2]?.result as readonly [bigint, bigint, bigint] | undefined; const mkt = r[3]?.result as readonly bigint[] | undefined; const supplied = pos && mkt && mkt[1]! > ZERO ? (pos[0] * mkt[0]!) / mkt[1]! : undefined; const wait = async (hash: `0x${string}`) => { const rc = await client?.waitForTransactionReceipt({ hash }); if (rc?.status === "reverted") throw new Error("Transaction failed"); }; const run = async (f: () => Promise<void>) => { try { await f(); } finally { setStep(null); reads.refetch(); } }; const lend = (amount: string) => run(async () => { if (!loop || !usdc || !address) return; const units = parseUnits(amount || "0", 6); if (units === ZERO) return; if ((allowance ?? ZERO) < units) { setStep("Approving USDC…"); await wait(await writeContractAsync({ address: usdc, abi: erc20Abi, functionName: "approve", args: [MORPHO_BLUE, units] })); } setStep("Lending…"); await wait(await writeContractAsync({ address: MORPHO_BLUE, abi: LEND_ABI, functionName: "supply", args: [parseMarket(loop.market), units, ZERO, address, "0x"] })); }); const withdrawAll = () => run(async () => { if (!loop || !address || !pos || pos[0] === ZERO) return; setStep("Withdrawing…"); await wait(await writeContractAsync({ address: MORPHO_BLUE, abi: LEND_ABI, functionName: "withdraw", args: [parseMarket(loop.market), ZERO, pos[0], address, address] })); }); return { address, step, isPending: step != null, walletUsdc: wallet == null ? undefined : Number(formatUnits(wallet, 6)), lentUsd: supplied == null ? undefined : Number(formatUnits(supplied, 6)), lend, withdrawAll, }; } const ORACLE_ABI = parseAbi(["function price() view returns (uint256)"]); /** Net rate on the borrowed dollars of a collateral route: the vault rate − the borrow rate + rewards. */ export const routeRate = (vaultRate: number | null | undefined, borrowRate: number | null | undefined, rewardsApr = 0) => vaultRate == null || borrowRate == null ? null : vaultRate - borrowRate + rewardsApr; /** The wallet's position on a collateral route, and one-transaction open / close (Bundler3, like the loop). The * collateral is priced with the market's own oracle, so the LTV chosen is the LTV Morpho sees. */ export function useMercatoCollateralRoute(data: MercatoData | undefined, id: string) { const { address } = useAccount(); const client = usePublicClient(); const { writeContractAsync } = useWriteContract(); const [step, setStep] = useState<string | null>(null); const route = data?.collateralRoutes?.find((r) => r.id === id); const vault = data?.vault; const live = !!(route?.marketId && route.market && vault && address); const reads = useReadContracts({ allowFailure: true, contracts: live ? [ { address: MORPHO_BLUE, abi: MORPHO_ABI, functionName: "position", args: [route!.marketId!, address!] }, { address: MORPHO_BLUE, abi: MORPHO_ABI, functionName: "market", args: [route!.marketId!] }, { address: MORPHO_BLUE, abi: MORPHO_ABI, functionName: "isAuthorized", args: [address!, GENERAL_ADAPTER_1] }, { address: route!.collateral.address, abi: erc20Abi, functionName: "allowance", args: [address!, GENERAL_ADAPTER_1] }, { address: route!.market!.oracle, abi: ORACLE_ABI, functionName: "price" }, ] : [], query: { enabled: live, refetchInterval: 30_000 }, }); // The collateral balance is useful before the market exists too (to show what you could put in). const bal = useReadContracts({ allowFailure: true, contracts: route && address ? [{ address: route.collateral.address, abi: erc20Abi, functionName: "balanceOf", args: [address] }] : [], query: { enabled: !!(route && address), refetchInterval: 30_000 }, }); const r = reads.data ?? []; const pos = r[0]?.result as readonly [bigint, bigint, bigint] | undefined; const mkt = r[1]?.result as readonly [bigint, bigint, bigint, bigint, bigint, bigint] | undefined; const authorized = r[2]?.result as boolean | undefined; const allowance = r[3]?.result as bigint | undefined; const oraclePrice = r[4]?.result as bigint | undefined; // 1e36 × USDC units per collateral unit const walletRaw = bal.data?.[0]?.result as bigint | undefined; const dec = route?.collateral.decimals ?? 18; const debtUnits = pos && mkt && mkt[3] > ZERO ? (pos[1] * mkt[2] + mkt[3] - BigInt(1)) / mkt[3] : ZERO; const E36 = BigInt(10) ** BigInt(36); const collateralUsd = pos && oraclePrice ? Number(formatUnits((pos[2] * oraclePrice) / E36, 6)) : 0; const debtUsd = Number(formatUnits(debtUnits, 6)); const collateralAmount = pos ? Number(formatUnits(pos[2], dec)) : 0; const position = collateralAmount > 0 ? { collateral: collateralAmount, collateralUsd, debtUsd, ltv: collateralUsd > 0 ? debtUsd / collateralUsd : null, /** The collateral price at which the position can be liquidated. */ liquidationPriceUsd: route && collateralAmount > 0 ? debtUsd / (collateralAmount * route.lltv) : null, } : null; const wait = async (hash: `0x${string}`) => { const rc = await client?.waitForTransactionReceipt({ hash }); if (rc?.status === "reverted") throw new Error("Transaction failed"); }; const sharePrice = async () => sharePriceE27(await client!.readContract({ address: vault!.address, abi: erc4626Abi, functionName: "convertToAssets", args: [BigInt(10) ** BigInt(18)] }), 18); async function authorize() { if (authorized) return; setStep("Allowing Morpho's adapter to borrow for you…"); await wait(await writeContractAsync({ address: MORPHO_BLUE, abi: MORPHO_ABI, functionName: "setAuthorization", args: [GENERAL_ADAPTER_1, true] })); } /** Put `amount` of the collateral in and borrow `ltv` of its value (at the oracle's price), held as mcUSDC. */ async function open(amount: string, ltv: number) { if (!route?.market || !vault || !address || !client || !oraclePrice) return; const collateral = parseUnits(amount || "0", dec); if (collateral === ZERO) return; const borrow = (((collateral * oraclePrice) / E36) * BigInt(Math.round(ltv * 10_000))) / BigInt(10_000); try { if ((allowance ?? ZERO) < collateral) { setStep(`Approving ${route.collateral.symbol}…`); await wait(await writeContractAsync({ address: route.collateral.address, abi: approveAbi, functionName: "approve", args: [GENERAL_ADAPTER_1, collateral] })); } await authorize(); setStep("Borrowing and supplying…"); await wait(await writeContractAsync(buildRoute({ user: address, vault: vault.address, market: parseMarket(route.market), collateralAssets: collateral, borrowAssets: borrow, sharePriceE27: await sharePrice() }))); } finally { setStep(null); reads.refetch(); bal.refetch(); } } /** Close the whole route: redeem enough of the wallet's mcUSDC to repay the debt (topping up from the wallet's USDC * if the mcUSDC is short), and take all the collateral back. Needs available liquidity in the vault. */ async function close() { if (!route?.market || !vault || !address || !client || debtUnits === ZERO) return; try { const price = await sharePrice(); const [mcBal, usdcBal] = await Promise.all([ client.readContract({ address: vault.address, abi: erc20Abi, functionName: "balanceOf", args: [address] }), client.readContract({ address: vault.asset, abi: erc20Abi, functionName: "balanceOf", args: [address] }), ]); const debtBuffered = (debtUnits * BigInt(10_010)) / BigInt(10_000) + BigInt(1); const sharesNeeded = (debtBuffered * BigInt(10) ** BigInt(27)) / price + BigInt(1); const shares = sharesNeeded < mcBal ? sharesNeeded : mcBal; const covered = (shares * price) / BigInt(10) ** BigInt(27); const topUp = covered >= debtBuffered ? ZERO : debtBuffered - covered; if (topUp > usdcBal) throw new Error("Not enough mcUSDC and USDC in the wallet to repay"); setStep("Approving mcUSDC…"); await wait(await writeContractAsync({ address: vault.address, abi: approveAbi, functionName: "approve", args: [GENERAL_ADAPTER_1, shares] })); if (topUp > ZERO) { setStep("Approving USDC…"); await wait(await writeContractAsync({ address: vault.asset, abi: approveAbi, functionName: "approve", args: [GENERAL_ADAPTER_1, topUp] })); } await authorize(); setStep("Closing…"); await wait(await writeContractAsync(buildUnroute({ user: address, vault: vault.address, market: parseMarket(route.market), shares, minSharePriceE27: (price * BigInt(9_990)) / BigInt(10_000), topUpAssets: topUp > ZERO ? topUp : undefined }))); } finally { setStep(null); reads.refetch(); bal.refetch(); } } return { route, address, position, open, close, step, isPending: step != null, walletCollateral: walletRaw == null ? null : Number(formatUnits(walletRaw, dec)), }; } export const fmtPct = (x: number | null | undefined, dp = 2) => (x == null || !Number.isFinite(x) ? "—" : `${(x * 100).toFixed(dp)}%`); export const fmtUsd = (x: number | null | undefined, dp = 0) => x == null || !Number.isFinite(x) ? "—" : `$${x.toLocaleString(undefined, { minimumFractionDigits: dp, maximumFractionDigits: dp })}`; export const RISK_LABEL = { 1: "Lower risk", 2: "Medium risk", 3: "Higher risk" } as const; /** The one line every Mercato surface carries (docs/positioning.md copy rules). */ export const MERCATO_DISCLOSURE = "Rates are variable and measured, never promised. Withdraw from available liquidity. mcUSDC is your share of Mercato USDC, a vault on Morpho. You can lose money.";lib/mercato-loop.ts import { encodeAbiParameters, encodeFunctionData, keccak256, parseAbi, type Address, type Hex } from "viem"; // Mercato kit — one-click loop (looping) on mcUSDC / USDC. https://mercato.sh/developers /* One-click loop on a Morpho market whose collateral is an ERC-4626 share of the loan asset (mcUSDC / USDC). Built on Morpho's Bundler3 + GeneralAdapter1 (morpho-org/bundler3): a flash loan of USDC, deposited into the vault with the user's own USDC, the shares supplied as collateral, and USDC borrowed to repay the flash loan — one transaction, no swap, so no price slippage. Bundler3 is marked deprecated in favour of Morpho's "Bundles", which have no flash loan or leverage function; Bundler3 stays deployed and immutable (docs/addresses.md). */ /** Source: docs.morpho.org/get-started/resources/addresses (Bundler3 section, Ethereum), read 2026-10-07; * GeneralAdapter1.BUNDLER3() checked on-chain. */ export const BUNDLER3 = "0x6566194141eefa99Af43Bb5Aa71460Ca2Dc90245" as const; export const GENERAL_ADAPTER_1 = "0x4A6c312ec70E8747a587EE860a0353cd42Be0aE0" as const; export const MORPHO_BLUE = "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb" as const; export type MarketParams = { loanToken: Address; collateralToken: Address; oracle: Address; irm: Address; lltv: bigint }; const MP = "(address loanToken, address collateralToken, address oracle, address irm, uint256 lltv)"; export const BUNDLER3_ABI = parseAbi(["struct Call { address to; bytes data; uint256 value; bool skipRevert; bytes32 callbackHash; }", "function multicall(Call[] bundle) payable"]); export const ADAPTER_ABI = parseAbi([ "function erc20TransferFrom(address token, address receiver, uint256 amount)", "function erc20Transfer(address token, address receiver, uint256 amount)", "function erc4626Deposit(address vault, uint256 assets, uint256 maxSharePriceE27, address receiver)", "function erc4626Redeem(address vault, uint256 shares, uint256 minSharePriceE27, address receiver, address owner)", "function morphoFlashLoan(address token, uint256 assets, bytes data)", `function morphoSupplyCollateral(${MP} marketParams, uint256 assets, address onBehalf, bytes data)`, `function morphoBorrow(${MP} marketParams, uint256 assets, uint256 shares, uint256 minSharePriceE27, address receiver)`, `function morphoRepay(${MP} marketParams, uint256 assets, uint256 shares, uint256 maxSharePriceE27, address onBehalf, bytes data)`, `function morphoWithdrawCollateral(${MP} marketParams, uint256 assets, address receiver)`, ]); export const MORPHO_ABI = parseAbi([ "function isAuthorized(address authorizer, address authorized) view returns (bool)", "function setAuthorization(address authorized, bool newIsAuthorized)", "function position(bytes32 id, address user) view returns (uint256 supplyShares, uint128 borrowShares, uint128 collateral)", "function market(bytes32 id) view returns (uint128 totalSupplyAssets, uint128 totalSupplyShares, uint128 totalBorrowAssets, uint128 totalBorrowShares, uint128 lastUpdate, uint128 fee)", ]); type Call = { to: Address; data: Hex; value: bigint; skipRevert: boolean; callbackHash: Hex }; const ZERO32 = `0x${"0".repeat(64)}` as Hex; const MAX = BigInt(2) ** BigInt(256) - BigInt(1); const E27 = BigInt(10) ** BigInt(27); const BPS = BigInt(10_000); const call = (data: Hex, callbackHash: Hex = ZERO32): Call => ({ to: GENERAL_ADAPTER_1, data, value: BigInt(0), skipRevert: false, callbackHash }); const fn = (functionName: any, args: readonly unknown[]) => encodeFunctionData({ abi: ADAPTER_ABI, functionName, args } as any); /** What GeneralAdapter1 passes to Bundler3.reenter in the flash-loan callback; Bundler3 checks keccak256 of it. */ const encodeCalls = (inner: Call[]): Hex => encodeAbiParameters( [{ type: "tuple[]", components: [{ name: "to", type: "address" }, { name: "data", type: "bytes" }, { name: "value", type: "uint256" }, { name: "skipRevert", type: "bool" }, { name: "callbackHash", type: "bytes32" }] }], [inner]); /** Leverage numbers for a target LTV: put in `deposit`, end with `deposit / (1 − ltv)` of collateral and borrow the rest. */ export function loopPlan(deposit: number, targetLtv: number, lltv: number, vaultApy: number, borrowApy: number) { const leverage = 1 / (1 - targetLtv); const collateral = deposit * leverage, borrow = collateral - deposit; return { leverage, collateral, borrow, netApy: vaultApy * leverage - borrowApy * (leverage - 1), /** How far the share price can fall before the position can be liquidated. */ dropToLiquidation: 1 - targetLtv / lltv, }; } /** The highest target LTV the app offers: 90% of LLTV (the same margin as `loopPresets` in the SDK). */ export const maxTargetLtv = (lltv: number) => lltv * 0.9; /** Bundler3.multicall args to open (or add to) a loop. `sharePriceE27` = assets per share × 1e27 (convertToAssets); * the deposit reverts if the vault's price is more than `slippageBps` above it. The user must first approve * `depositAssets` USDC to GeneralAdapter1 and authorize it on Morpho (`loopPrerequisites`). */ export function buildLoop(p: { user: Address; vault: Address; market: MarketParams; depositAssets: bigint; borrowAssets: bigint; sharePriceE27: bigint; slippageBps?: number }) { const slip = BigInt(p.slippageBps ?? 10); const inner: Call[] = [ call(fn("erc4626Deposit", [p.vault, MAX, (p.sharePriceE27 * (BPS + slip)) / BPS, GENERAL_ADAPTER_1])), call(fn("morphoSupplyCollateral", [p.market, MAX, p.user, "0x"])), call(fn("morphoBorrow", [p.market, p.borrowAssets, BigInt(0), BigInt(0), GENERAL_ADAPTER_1])), ]; const data = encodeCalls(inner); const bundle: Call[] = [ call(fn("erc20TransferFrom", [p.market.loanToken, GENERAL_ADAPTER_1, p.depositAssets])), call(fn("morphoFlashLoan", [p.market.loanToken, p.borrowAssets, data]), keccak256(data)), ]; return { address: BUNDLER3, abi: BUNDLER3_ABI, functionName: "multicall" as const, args: [bundle] as const }; } /** Bundler3.multicall args to close a loop entirely: flash-borrow the debt, repay it, take the collateral back, * redeem it in the vault and send what is left to the user as USDC. Needs the vault to have available liquidity * for the redeem; `debtAssets` should include a small buffer over the current debt (the unused part comes back). */ export function buildUnloop(p: { user: Address; vault: Address; market: MarketParams; debtAssets: bigint; minSharePriceE27: bigint }) { const inner: Call[] = [ call(fn("morphoRepay", [p.market, BigInt(0), MAX, MAX, p.user, "0x"])), call(fn("morphoWithdrawCollateral", [p.market, MAX, GENERAL_ADAPTER_1])), call(fn("erc4626Redeem", [p.vault, MAX, p.minSharePriceE27, GENERAL_ADAPTER_1, GENERAL_ADAPTER_1])), ]; const data = encodeCalls(inner); const bundle: Call[] = [ call(fn("morphoFlashLoan", [p.market.loanToken, p.debtAssets, data]), keccak256(data)), call(fn("erc20Transfer", [p.market.loanToken, p.user, MAX])), ]; return { address: BUNDLER3, abi: BUNDLER3_ABI, functionName: "multicall" as const, args: [bundle] as const }; } /** Share price in the units GeneralAdapter1 checks: assets per share, scaled by 1e27. */ export const sharePriceE27 = (assetsPerOneShare: bigint, shareDecimals: number) => (assetsPerOneShare * E27) / BigInt(10) ** BigInt(shareDecimals); /* ---- Collateral routes: `AVT → mcUSDC` (docs/avt.md §2.2), later `cbBTC → mcUSDC` (markets-roadmap row 8) ---- Supply the user's collateral to a Morpho market that lends USDC, borrow USDC, deposit it into the vault for the user — one transaction, no flash loan, no swap. The user keeps the collateral exposure and holds mcUSDC; the net rate on the borrowed dollars is the vault rate − the borrow rate (+ any rewards). */ /** Numbers for a route at a target LTV: `collateralUsd` of collateral, borrow `targetLtv` of it. */ export function routePlan(collateralUsd: number, targetLtv: number, lltv: number, vaultApy: number, borrowApy: number, rewardsApr = 0) { const borrow = collateralUsd * targetLtv; const spread = vaultApy - borrowApy; return { borrow, /** Net rate on the borrowed dollars, and the same in dollars a year as a share of the collateral. */ netOnBorrowed: spread + rewardsApr, netOnCollateral: (spread + rewardsApr) * targetLtv, /** How far the collateral's price can fall before the position can be liquidated. */ dropToLiquidation: 1 - targetLtv / lltv, }; } /** Bundler3.multicall args to open a route. Prerequisites: approve `collateralAssets` of the collateral token to * GeneralAdapter1 and authorize it on Morpho (`loopPrerequisites`). The deposit reverts if the vault's share price is * more than `slippageBps` above `sharePriceE27`. */ export function buildRoute(p: { user: Address; vault: Address; market: MarketParams; collateralAssets: bigint; borrowAssets: bigint; sharePriceE27: bigint; slippageBps?: number }) { const slip = BigInt(p.slippageBps ?? 10); const bundle: Call[] = [ call(fn("erc20TransferFrom", [p.market.collateralToken, GENERAL_ADAPTER_1, p.collateralAssets])), call(fn("morphoSupplyCollateral", [p.market, MAX, p.user, "0x"])), call(fn("morphoBorrow", [p.market, p.borrowAssets, BigInt(0), BigInt(0), GENERAL_ADAPTER_1])), call(fn("erc4626Deposit", [p.vault, MAX, (p.sharePriceE27 * (BPS + slip)) / BPS, p.user])), ]; return { address: BUNDLER3, abi: BUNDLER3_ABI, functionName: "multicall" as const, args: [bundle] as const }; } /** Bundler3.multicall args to close a route entirely: redeem `shares` of the user's mcUSDC, repay the whole debt, * return all the collateral and any USDC left over. If the redeemed USDC is short of the debt (the borrow rate ran * above the vault rate), `topUpAssets` of the user's USDC covers it; the unused part comes back. Prerequisites: * approve `shares` mcUSDC (and `topUpAssets` USDC) to GeneralAdapter1; it is already authorized on Morpho. Needs * the vault to have available liquidity for the redeem. */ export function buildUnroute(p: { user: Address; vault: Address; market: MarketParams; shares: bigint; minSharePriceE27: bigint; topUpAssets?: bigint }) { const bundle: Call[] = [ call(fn("erc20TransferFrom", [p.vault, GENERAL_ADAPTER_1, p.shares])), call(fn("erc4626Redeem", [p.vault, MAX, p.minSharePriceE27, GENERAL_ADAPTER_1, GENERAL_ADAPTER_1])), ...(p.topUpAssets ? [call(fn("erc20TransferFrom", [p.market.loanToken, GENERAL_ADAPTER_1, p.topUpAssets]))] : []), call(fn("morphoRepay", [p.market, BigInt(0), MAX, MAX, p.user, "0x"])), call(fn("morphoWithdrawCollateral", [p.market, MAX, p.user])), call(fn("erc20Transfer", [p.market.loanToken, p.user, MAX])), ]; return { address: BUNDLER3, abi: BUNDLER3_ABI, functionName: "multicall" as const, args: [bundle] as const }; }components/mercato-loop.tsx "use client"; // Mercato kit — one-click loop: supply USDC, borrow against the mcUSDC and supply again, in one transaction. // https://mercato.sh/developers · Pass your own wallet button as `connect`. import { useEffect, useState, type ReactNode } from "react"; import { cn } from "@/lib/utils"; import { MERCATO_DISCLOSURE, fmtPct, fmtUsd, loopCushion, loopRate, useMercato, useMercatoLoop } from "@/lib/mercato"; /** `amount` (optional) controls the amount from outside — e.g. the Earn block — and hides this block's own input. */ export function MercatoLoop({ api, connect, defaultAmount = "1000", amount: controlled, className }: { api?: string; connect?: ReactNode; defaultAmount?: string; amount?: string; className?: string }) { const { data, isLoading } = useMercato(api); const l = useMercatoLoop(data); const loop = data?.loop; const presets = loop?.presets ?? []; const [own, setAmount] = useState(defaultAmount); const amount = controlled ?? own; const [leverage, setLeverage] = useState<number>(); const [failed, setFailed] = useState<string | null>(null); useEffect(() => { if (leverage == null && presets.length) setLeverage(presets[Math.min(1, presets.length - 1)]); }, [leverage, presets]); const n = Number(amount) || 0; const lev = leverage ?? 1; const borrow = n * (lev - 1); const rate = loopRate(data?.vault.rate, loop?.borrowRate, lev); const short = l.walletUsdc != null && n > l.walletUsdc; const noRoom = loop != null && borrow > loop.liquidityUsd; const run = async (f: () => Promise<void>) => { setFailed(null); try { await f(); } catch (e) { setFailed(e instanceof Error && /reject|denied/i.test(e.message) ? "Cancelled in your wallet." : "Something went wrong — nothing moved unless your wallet shows it. Try again."); } }; if (isLoading) return <div className={cn("h-96 animate-pulse rounded-xl bg-secondary", className)} />; if (!loop) return null; const pos = l.position; return ( <div className={cn("flex flex-col gap-4 rounded-xl border border-border bg-card p-5 text-card-foreground", className)}> <div> <div className="font-medium">Boost with a loop</div> <p className="mt-1 text-sm text-muted-foreground">Supply USDC, borrow against your mcUSDC and supply that too — in one transaction. More of your money earns, and it can be liquidated if mcUSDC's price falls.</p> </div> {pos && ( <div className="flex flex-col gap-3 rounded-lg bg-secondary/60 p-3 text-sm"> <div className="grid grid-cols-3 gap-2"> <div><div className="text-xs text-muted-foreground">Yours</div><div className="mercato-figure tabular-nums">{fmtUsd(pos.equityUsd, 2)}</div></div> <div><div className="text-xs text-muted-foreground">Leverage</div><div className="mercato-figure tabular-nums">{pos.leverage ? `${pos.leverage.toFixed(2)}×` : "—"}</div></div> <div><div className="text-xs text-muted-foreground">Borrowed</div><div className="mercato-figure tabular-nums">{fmtUsd(pos.debtUsd, 2)}</div></div> </div> {pos.leverage && <div className="text-xs text-muted-foreground">Can be liquidated if mcUSDC falls {fmtPct(loopCushion(pos.leverage, loop.lltv), 1)} from here.</div>} <button type="button" disabled={l.isPending} onClick={() => run(l.close)} className="h-10 rounded-lg border border-border font-medium transition hover:bg-secondary disabled:opacity-40">{l.step === "Closing…" ? "Closing…" : "Close loop — get USDC back"}</button> </div> )} {controlled == null && <div className="flex flex-col gap-1.5"> <label className="flex items-center gap-2 rounded-lg border border-border bg-background/40 px-3 py-2.5"> <span className="text-2xl text-muted-foreground">$</span> <input inputMode="decimal" value={amount} onChange={(e) => { setAmount(e.target.value.replace(/[^0-9.]/g, "")); setFailed(null); }} className="mercato-figure w-full min-w-0 bg-transparent text-2xl tabular-nums outline-none" placeholder="0" aria-label="Amount in USDC" /> <span className="text-sm text-muted-foreground">USDC</span> </label> {l.walletUsdc != null && <div className="text-xs text-muted-foreground">In your wallet: <span className="mercato-figure tabular-nums">{fmtUsd(l.walletUsdc, 2)}</span></div>} </div>} <div role="radiogroup" aria-label="Leverage" className="grid gap-1.5" style={{ gridTemplateColumns: `repeat(${presets.length}, minmax(0, 1fr))` }}> {presets.map((p) => ( <button key={p} type="button" role="radio" aria-checked={p === leverage} onClick={() => setLeverage(p)} className={cn("flex flex-col items-center rounded-lg py-2 text-sm transition", p === leverage ? "bg-primary text-primary-foreground" : "bg-secondary text-secondary-foreground hover:bg-secondary/80")}> <span className="font-medium">{p}×</span> <span className={cn("text-xs", p === leverage ? "opacity-80" : "text-muted-foreground")}>{fmtPct(loopRate(data?.vault.rate, loop.borrowRate, p), 1)}</span> </button> ))} </div> <dl className="grid grid-cols-2 gap-x-4 gap-y-2 text-sm"> <dt className="text-muted-foreground">Earning on</dt><dd className="mercato-figure text-right tabular-nums">{fmtUsd(n * lev, 2)}</dd> <dt className="text-muted-foreground">Borrowed</dt><dd className="mercato-figure text-right tabular-nums">{fmtUsd(borrow, 2)} <span className="font-sans">at</span> {fmtPct(loop.borrowRate, 2)}</dd> <dt className="text-muted-foreground">Net rate</dt><dd className="mercato-figure text-right tabular-nums">{fmtPct(rate, 2)} <span className="font-sans text-muted-foreground">variable</span></dd> <dt className="text-muted-foreground">Liquidated if mcUSDC falls</dt><dd className="mercato-figure text-right tabular-nums">{fmtPct(loopCushion(lev, loop.lltv), 1)}</dd> </dl> {!loop.available ? ( <button type="button" disabled className="h-12 rounded-lg bg-primary font-medium text-primary-foreground opacity-40">Opens at launch</button> ) : !l.address ? (connect ?? <div className="text-sm text-muted-foreground">Connect a wallet to continue.</div>) : ( <button type="button" disabled={l.isPending || n <= 0 || short || noRoom} onClick={() => run(() => l.open(amount, lev))} className="h-12 rounded-lg bg-primary font-medium text-primary-foreground transition disabled:opacity-40"> {l.step ?? (short ? "Not enough USDC in your wallet" : noRoom ? "Not enough to borrow right now" : `Loop ${fmtUsd(n, 2)} at ${lev}×`)} </button> )} {l.step && l.step !== "Looping…" && <p className="text-xs text-muted-foreground">The first time, your wallet asks you to approve USDC and to let Morpho's adapter borrow for you; then the loop itself.</p>} {failed && <p className="text-xs text-destructive">{failed}</p>} <p className="text-xs text-muted-foreground">If the borrow rate rises above what mcUSDC earns, a loop earns less than holding. {MERCATO_DISCLOSURE}</p> </div> ); }Update the import paths to match your project setup
The files import
@/lib/utils(shadcn'scn),@/lib/mercatoand@/components/….
Usage
import { MercatoLoop } from "@/components/mercato-loop"
import { ConnectButton } from "@rainbow-me/rainbowkit"
export function Boost() {
return <MercatoLoop connect={<ConnectButton />} />
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
connect | ReactNode | — | Your wallet button, shown when no wallet is connected. |
defaultAmount | string | "1000" | Starting amount in the input. |
amount | string | — | Controls the amount from outside and hides the input (as the Earn block does). |
api | string | provider | Overrides the API for this component. |
className | string | — | Classes on the outer card. |