diff --git a/apps/web/public/chains/11155111.png b/apps/web/public/chains/11155111.png new file mode 100644 index 0000000..bf957d3 Binary files /dev/null and b/apps/web/public/chains/11155111.png differ diff --git a/apps/web/public/tokens/11155111/0x0000000000000000000000000000000000000000.png b/apps/web/public/tokens/11155111/0x0000000000000000000000000000000000000000.png new file mode 100644 index 0000000..bf957d3 Binary files /dev/null and b/apps/web/public/tokens/11155111/0x0000000000000000000000000000000000000000.png differ diff --git a/apps/web/public/tokens/11155111/0x1c7d4b196cb0c7b01d743fbc6116a902379c7238.svg b/apps/web/public/tokens/11155111/0x1c7d4b196cb0c7b01d743fbc6116a902379c7238.svg new file mode 100644 index 0000000..0fac44c --- /dev/null +++ b/apps/web/public/tokens/11155111/0x1c7d4b196cb0c7b01d743fbc6116a902379c7238.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/web/public/tokens/11155111/0xb82381a3fbd3fafa77b3a7be693342618240067b.png b/apps/web/public/tokens/11155111/0xb82381a3fbd3fafa77b3a7be693342618240067b.png new file mode 100644 index 0000000..49ab895 Binary files /dev/null and b/apps/web/public/tokens/11155111/0xb82381a3fbd3fafa77b3a7be693342618240067b.png differ diff --git a/apps/web/src/app/api/ref/spot/route.ts b/apps/web/src/app/api/ref/spot/route.ts new file mode 100644 index 0000000..08923d3 --- /dev/null +++ b/apps/web/src/app/api/ref/spot/route.ts @@ -0,0 +1,30 @@ +/** + * Display-only mainnet spot price, for networks without a market worth quoting (Sepolia: ETH / USD for WETH / USDC and, + * times wstETH's own rate, wstETH / USDC). GET /api/ref/spot?symbol=ETH_USD -> { price, source }. Asks Coinbase, then + * Kraken; each answer is cached for 30 s, and when both fail the route answers `{ price: null }` (status 502) so the + * screens simply show no reference. Never used on-chain. + */ +import { SPOT_PAIRS, coinbaseSpot, krakenLast } from "@/lib/spot"; + +const TTL_S = 30; + +async function ask(url: string, parse: (j: unknown) => number | null): Promise { + try { + const r = await fetch(url, { next: { revalidate: TTL_S }, signal: AbortSignal.timeout(4_000) }); + return r.ok ? parse(await r.json()) : null; + } catch { + return null; + } +} + +export async function GET(req: Request) { + const symbol = new URL(req.url).searchParams.get("symbol") ?? ""; + const pair = Object.hasOwn(SPOT_PAIRS, symbol) ? SPOT_PAIRS[symbol] : undefined; // not toString / __proto__ + if (!pair) return Response.json({ price: null }, { status: 400 }); + const cache = { "cache-control": `public, max-age=${TTL_S}` }; + const cb = await ask(`https://api.coinbase.com/v2/prices/${pair.coinbase}/spot`, coinbaseSpot); + if (cb !== null) return Response.json({ price: cb, source: "coinbase" }, { headers: cache }); + const kr = await ask(`https://api.kraken.com/0/public/Ticker?pair=${pair.kraken}`, krakenLast); + if (kr !== null) return Response.json({ price: kr, source: "kraken" }, { headers: cache }); + return Response.json({ price: null }, { status: 502 }); +} diff --git a/apps/web/src/app/specials/page.tsx b/apps/web/src/app/specials/page.tsx new file mode 100644 index 0000000..fff658b --- /dev/null +++ b/apps/web/src/app/specials/page.tsx @@ -0,0 +1,13 @@ +import type { Metadata } from "next"; +import { Suspense } from "react"; +import { SpecialsScreen } from "@/components/specials/SpecialsScreen"; + +export const metadata: Metadata = { title: "Specials | Confluere" }; + +export default function Page() { + return ( + + + + ); +} diff --git a/apps/web/src/components/borrow/LendForm.tsx b/apps/web/src/components/borrow/LendForm.tsx index 5271471..a6e90e1 100644 --- a/apps/web/src/components/borrow/LendForm.tsx +++ b/apps/web/src/components/borrow/LendForm.tsx @@ -8,7 +8,7 @@ import Link from "next/link"; import { useState } from "react"; import { useConnection } from "wagmi"; import type { Hex } from "viem"; -import { quoteDown, scaleOf } from "@confluere/sdk"; +import { quoteDown, quoteUp, scaleOf, tradeFee } from "@confluere/sdk"; import { useActiveChain } from "@/lib/activeChain"; import { launchTokensOf } from "@/lib/chains"; import { useAccountOrders, useMinNotional, usePools, useTokens, tokenOf, type PoolRow } from "@/lib/data"; @@ -134,10 +134,10 @@ function LendFormFor({ pool, onClose, embedded }: { pool: PoolRow; onClose?: () // facts per 1 unit of collateral until an amount is typed (Pendle PT panel: rate, maturity, payout before input) const units = amt > 0n && q.units > 0n ? q.units : 10n ** BigInt(A.decimals); - const paid = amt > 0n ? amt : quoteDown(units, price, scale); + const fee = tradeFee(quoteUp(units, price, scale)); // v0.8: 0.015% on the lender's side, included in the amount + const paid = amt > 0n ? amt : quoteUp(units, price, scale) + fee; const payout = quoteDown(units, f.netPerUnit || bbPrice, scale); const per = amt > 0n ? "" : ` per 1 ${A.symbol}`; - const fee = (quoteDown(units, bbPrice, scale) - payout); const ft = positionName("ft", pool, A, Q, { all: allPools ?? [] }); return ( @@ -164,12 +164,12 @@ function LendFormFor({ pool, onClose, embedded }: { pool: PoolRow; onClose?: () 0n && q.units > 0n && { k: "You receive", v: ≈ {fmtToken(q.units, { symbol: ft.short, decimals: A.decimals })}, hint: `1 C = 1 ${A.symbol} of backing`, testId: "lend-receive" }, - { k: "Fixed rate", v: price > 0n ? rateText(f, pool.expiry - now) : "—", hint: "net of the protocol's share" }, + { k: "Fixed rate", v: price > 0n ? rateText(f, pool.expiry - now) : "—", hint: "net of the 0.015% trade fee" }, { k: `You pay${per}`, v: <>≈ {fmtToken(paid, Q)}, hint: price > 0n ? `${fmtPer(price, Q)} ${Q.symbol} per ${A.symbol}` : undefined }, { k: `Paid back${per}`, v: fmtToken(payout, Q), hint: `on ${fmtChip(pool.expiry)} UTC, if repaid` }, !own && { k: "Worst case", v: worst.priced ? rateText(worst, pool.expiry - now) : "—", hint: `you pay at most ${fmtPer(cap, Q)} per ${A.symbol}` }, { k: "Collateral", v: `${A.symbol}, bought back at ${fmtPer(bbPrice, Q)}`, hint: refUnits ? `${pct(Number(bbPrice - refUnits) / Number(refUnits), 1)} vs market (reference)` : undefined }, - { k: "Fee", v: fee > 0n ? fmtToken(fee, Q) : "None", hint: "share of the interest, only if repaid" }, + { k: "Trade fee", v: fee > 0n ? fmtToken(fee, Q) : "None", hint: "0.015%, included in what you pay; the 5% share of the interest comes from the borrower's side", testId: "lend-fee" }, { k: "Repayment", v: repaysAnyTime(pool) ? "Any time" : "At maturity", hint: repaysAnyTime(pool) ? "you may be paid back early" : undefined }, ]} /> diff --git a/apps/web/src/components/borrow/LoopCard.tsx b/apps/web/src/components/borrow/LoopCard.tsx index c53416c..273fc8a 100644 --- a/apps/web/src/components/borrow/LoopCard.tsx +++ b/apps/web/src/components/borrow/LoopCard.tsx @@ -117,8 +117,8 @@ export function LoopCard({ pool, A, Q, embedded = false, top }: { pool: PoolRow; onMax={address && spendable ? () => setStart(toInput(spendable, A.decimals)) : undefined} /> {short && stake && ( -

- You need {A.symbol}. Stake {deployment ? Q.symbol.replace(/^W/, "") : ""} at FastLane (opens their site). +

+ You need {A.symbol}. {stake.href ? <>Stake {deployment ? Q.symbol.replace(/^W/, "") : ""} at {stake.provider} (opens their site). : stake.note} {stake.href && ( Open @@ -189,7 +189,7 @@ export function LoopCard({ pool, A, Q, embedded = false, top }: { pool: PoolRow;

  1. 1 Borrow {Q.symbol}
  2. 2 Withdraw it from Portfolio
  3. -
  4. {staking ? "3 Stake at FastLane (their site)" : `3 Turn it into ${P.symbol} where you usually trade`}
  5. +
  6. {staking ? (stake ? `3 Stake at ${stake.provider}${stake.href ? " (their site)" : `: send ${Q.symbol.replace(/^W/, "")} to the ${A.symbol} contract`}` : "3 Stake it (outside Confluere)") : `3 Turn it into ${P.symbol} where you usually trade`}
{plan &&

If you don't repay: you keep the {Q.symbol}, lenders keep the {A.symbol} you sold. No liquidation.

} @@ -258,7 +258,7 @@ export function LoopCard({ pool, A, Q, embedded = false, top }: { pool: PoolRow;
  1. 1 Borrow {Q.symbol}
  2. 2 Withdraw it from Portfolio
  3. -
  4. {staking ? "3 Stake at FastLane (their site)" : `3 Turn it into ${P.symbol} where you usually trade`}
  5. +
  6. {staking ? (stake ? `3 Stake at ${stake.provider}${stake.href ? " (their site)" : `: send ${Q.symbol.replace(/^W/, "")} to the ${A.symbol} contract`}` : "3 Stake it (outside Confluere)") : `3 Turn it into ${P.symbol} where you usually trade`}
{!open ? ( diff --git a/apps/web/src/components/borrow/borrowFlow.tsx b/apps/web/src/components/borrow/borrowFlow.tsx index c164dd6..c02058e 100644 --- a/apps/web/src/components/borrow/borrowFlow.tsx +++ b/apps/web/src/components/borrow/borrowFlow.tsx @@ -17,7 +17,7 @@ import { launchTokensOf } from "@/lib/chains"; import { useAccountOrders, useBlockTime, useMinNotional, type PoolRow, type Token } from "@/lib/data"; import { useBalances, usePending, usePositions } from "@/lib/position"; import { useActionCtx, useChainClock } from "@/lib/useProduct"; -import { borrowQuote, defaultBorrowLimit, defaultMoveBps, minUnits, refToUnits, type BorrowQuote } from "@/lib/quote"; +import { borrowQuote, defaultBorrowLimit, defaultMoveBps, minUnits, netAtPrice, refToUnits, type BorrowQuote } from "@/lib/quote"; import { repayDeadline, type Deadline } from "@/lib/deadline"; import { borrowWrites } from "@/lib/actions"; import { defaultTtl } from "@/lib/order"; @@ -115,7 +115,8 @@ export function useBorrowOrder({ pool, A, Q, amount, ref, payNative = false, rec const limitTooHigh = ownPrice && !!pool && limit >= bbPrice; const limitLow = ownPrice && !!anchor && limit > 0n && limit < (anchor * 9n) / 10n; const scale = pool ? scaleOf(pool.assetDecimals) : 1n; - const floor = q && pool && !ownPrice ? quoteDown(show, q.guard, scale) : undefined; + // net of the v0.8 fees at the guard price: 0.015% trade fee and 5% of (buy-back − price) + const floor = q && pool && !ownPrice ? netAtPrice(pool, show, q.guard) : undefined; const sym = payToken?.symbol ?? ""; let label: string; @@ -256,9 +257,9 @@ export function BorrowTerms({ o, testId = "borrow-terms", collapsible = false, t { k: `You receive${per}`, v: <>{o.ownPrice ? "at least " : "≈ "}{fmtToken(qs.receive, Q)}, hint: o.floor !== undefined && o.units > 0n ? `at least ${fmtToken(o.floor, Q)} (price may move ${o.move / 100}%)` : undefined }, { k: `Repay${per}`, v: fmtToken(qs.repay, Q), testId: "row-repay" }, { k: "Repay by", v: , hint: left > 0 ? `in ${fmtWait(left)}${repaysAnyTime(pool) ? ", any time before" : ", only after maturity"}` : undefined }, - { k: "Interest", v: fmtToken(qs.repay - qs.receive > 0n ? qs.repay - qs.receive : 0n, Q) }, + { k: "Interest", v: fmtToken(qs.repay - qs.receive > 0n ? qs.repay - qs.receive : 0n, Q), hint: "fees included" }, vsMarket !== undefined && { k: "Buy-back price", v: `${fmtPer(o.bbPrice, Q)} ${Q.symbol}`, hint: `${vsMarket >= 0 ? "+" : ""}${pct(vsMarket, 1)} vs market (reference)` }, - { k: "Fee", v: "None" }, + { k: "Fees", v: fmtToken(qs.tradeFee + qs.spreadShare, Q), hint: `0.015% trade fee ${fmtToken(qs.tradeFee, Q)} + 5% of the interest ${fmtToken(qs.spreadShare, Q)}; nothing on repay`, testId: "row-fees" }, ]} /> ); @@ -333,7 +334,7 @@ export function BorrowReview({ o, onPlaced }: { o: BorrowOrder; onPlaced?: () => { k: "Repay by", v: , hint: anyTime ? "any time before" : "only after maturity" }, o.floor !== undefined && { k: "You receive at least", v: fmtToken(o.floor, Q), hint: "or the order waits for the next round" }, { k: "Valid for", v: `${o.tries} round${o.tries > 1 ? "s" : ""}`, hint: `≈ ${fmtWait(o.tries * roundSeconds(pool))}` }, - { k: "Fee", v: "None" }, + { k: "Fees", v: fmtToken(q.tradeFee + q.spreadShare, Q), hint: "0.015% trade fee + 5% of the interest, taken from what you receive; nothing on repay" }, ]} /> Vaults setVault(a, { pay })} />

- Rates are what the vault would earn right now, after the protocol's share of the interest. Prefer to choose a maturity yourself?{" "} + Rates are what the vault would earn right now, after the 0.015% trade fee. Withdrawals are paid in the loan token: vaults lend only into auto-sell markets, so defaulted collateral is sold in the next maturity's rounds first. Prefer to choose a maturity yourself?{" "} Lend in one market diff --git a/apps/web/src/components/earn/VaultDrawer.tsx b/apps/web/src/components/earn/VaultDrawer.tsx index c0a9798..b3e1bf5 100644 --- a/apps/web/src/components/earn/VaultDrawer.tsx +++ b/apps/web/src/components/earn/VaultDrawer.tsx @@ -209,7 +209,7 @@ function VaultBody({ v, quote, asset, role, native, wrapped = false }: { v: Vaul } /> {rw.picker} -

Withdrawals are requested now and paid at the end of this period ({periodEnd}), first come first served. If the vault's funds are out on loan, they are paid when those loans reach maturity.

+

Withdrawals are requested now and paid at the end of this period ({periodEnd}), first come first served. If the vault's funds are out on loan, they are paid when those loans reach maturity. You are paid in the loan token: the vault lends only into markets that auto-sell defaulted collateral in the next maturity's rounds; any small part that does not sell is claimable by share.

{me && me.pendingRedeem > 0n && Withdrawal requested · payable after {ended ? "the next roll" : }.} {me && me.claimAssets > 0n && ( diff --git a/apps/web/src/components/layout/NotDeployed.tsx b/apps/web/src/components/layout/NotDeployed.tsx new file mode 100644 index 0000000..a2667b1 --- /dev/null +++ b/apps/web/src/components/layout/NotDeployed.tsx @@ -0,0 +1,53 @@ +"use client"; + +/** + * A network the app serves that has no deployment yet (Sepolia until deployments/sepolia.json exists and the SDK is + * regenerated). Instead of empty screens and failing reads, every page that needs the contracts shows one calm state: + * what is true, the networks that are live, and the demo (which has a full Sepolia sample network). Nothing on this + * page reads the chain or the server index, so there is no retry line and no error. The state disappears by itself on + * the build that carries the deployment (lib/chains `deploymentOf`). + */ +import Link from "next/link"; +import { useActiveChain, useSwitchNetwork } from "@/lib/activeChain"; +import { ENABLED, chainMeta, deploymentOf } from "@/lib/chains"; +import { DEMO_PARAM } from "@/lib/demo/mode"; +import { networkName } from "@/lib/vocab"; +import { Button } from "../ui/Button"; +import { EmptyState } from "../ui/Surface"; + +/** Pages that work without a deployment: how it works, and the demo entry (its sample network has every chain). */ +export const worksUndeployed = (path: string) => path.startsWith("/how-it-works") || path.startsWith("/developers/demo"); + +export function NotDeployed() { + const { meta } = useActiveChain(); + const { switchNetwork } = useSwitchNetwork(); + const live = ENABLED.filter((c) => c.id !== meta.id && !!deploymentOf(c.id)); + return ( +
+ +

+ Confluere supports {networkName(meta)}, but its contracts are not deployed there yet. Markets, vaults and your positions on {meta.name} appear here as + soon as they are. +

+

{live.length > 0 ? `Meanwhile you can use ${live.map((c) => chainMeta(c.id).label).join(" or ")}, or look around in the demo.` : "Meanwhile you can look around in the demo."}

+ + } + actions={ + <> + {live.map((c) => ( + + ))} + + Try the demo on {meta.name} + + + } + /> +
+ ); +} diff --git a/apps/web/src/components/layout/nav.ts b/apps/web/src/components/layout/nav.ts index 14af118..9ca4905 100644 --- a/apps/web/src/components/layout/nav.ts +++ b/apps/web/src/components/layout/nav.ts @@ -26,6 +26,7 @@ export const TABS: NavItem[] = [ /** The desktop More menu: the sample-data Demo above Developers. */ export const MORE: { href: string; label: string }[] = [ + { href: "/specials", label: "Specials" }, { href: "/developers/demo", label: "Demo" }, { href: "/developers", label: "Developers" }, ]; diff --git a/apps/web/src/components/markets/PoolDrawer.tsx b/apps/web/src/components/markets/PoolDrawer.tsx index 78fbeb2..6cc426b 100644 --- a/apps/web/src/components/markets/PoolDrawer.tsx +++ b/apps/web/src/components/markets/PoolDrawer.tsx @@ -18,7 +18,7 @@ import { poolDue } from "@/lib/due"; import { netReturn, rateText, refToUnits } from "@/lib/quote"; import { ADVANCE_LABEL, advanceWrite, offerHeld, offerWrites } from "@/lib/proActions"; import { windowLeft } from "@/lib/deadline"; -import { fmtChip, fmtClock, fmtExpiry, fmtPer, fmtToken, fmtWait, parseAmount } from "@/lib/format"; +import { fmtChip, fmtClock, fmtExpiry, fmtPer, fmtToken, fmtWait, parseAmount, pct } from "@/lib/format"; import { buyBack, lastTraded, misses, nextRoundAt, poolState, priceGuardBps, roundSeconds, unitScale } from "@/lib/view"; import { positionName, priceSource, type PositionKind } from "@/lib/vocab"; import { sortBest, tokenIdOf, usePositionBook } from "@/lib/secondary"; @@ -83,7 +83,8 @@ function PoolBody({ pool, A, Q }: { pool: PoolRow; A: Token; Q: Token }) { now ? `in ${fmtWait(pool.expiry - now)} (UTC)` : "UTC"} /> - + + 0n ? fmtToken(pool.feeEscrow, Q, true, { compact: true }) : "—"} note={pool.filledUnits > 0n ? `${pct(Number(pool.repaidUnits) / Number(pool.filledUnits), 0)} repaid so far; the unpaid part goes to lenders at close` : "split at close by the share repaid"} />
diff --git a/apps/web/src/components/more/CreateMarketCard.tsx b/apps/web/src/components/more/CreateMarketCard.tsx index 790a720..86381dc 100644 --- a/apps/web/src/components/more/CreateMarketCard.tsx +++ b/apps/web/src/components/more/CreateMarketCard.tsx @@ -4,18 +4,17 @@ import { useEffect, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import { isAddress, type Address } from "viem"; -import { Kind, cadenceMultiplier, isGridStrike, poolId } from "@confluere/sdk"; +import { Kind, cadenceMultiplier, poolId } from "@confluere/sdk"; import { useActiveChain } from "@/lib/activeChain"; import { onchainSymbol, tokenOf, useGrid, usePools, usePoolStatus, useTokens } from "@/lib/data"; import { isNative, isWrappedNative, nativeTokenOf, NATIVE, useNativeBalance, wrappedNativeOf } from "@/lib/native"; import { tokenName } from "@/lib/logos"; import { launchTokens, wrapWrite } from "@/lib/tokens"; import { useBalances } from "@/lib/position"; -import { BAND_TIERS, FEE_TIERS, gridExpiries, nearestGridStrikes } from "@/lib/grid"; +import { BAND_TIERS, gridExpiries, isLogGridStrike, nearestGridStrikes, standardBand } from "@/lib/grid"; import { fmtChip, fmtPer, fmtToken, fmtWait, parseAmount, toInput } from "@/lib/format"; import { marketKey, openMarketWrite } from "@/lib/proActions"; import { STYLE_LABEL } from "@/lib/vocab"; -import { priceDigits } from "@/lib/view"; import { useTx, type Write } from "@/lib/tx"; import { AmountInput, Button, Card, ChipRow, Disclosure, Segmented, TokenButton, TokenSelect, TxErrorLine, useLiveNow } from "../ui"; import { WalletAction } from "../layout/WalletMenu"; @@ -36,7 +35,10 @@ export function CreateMarketCard() { const [kind, setKind] = useState(Kind.American); const [expiry, setExpiry] = useState(); const [bbStr, setBb] = useState(""); - const [fee, setFee] = useState(1000); + // pre-v0.8 cores still take a feeBps argument; v0.8 fixes the spread share at 5% and ignores the tier + const fee = 1000; + const [autoSell, setAutoSell] = useState(true); + const [guardTouched, setGuardTouched] = useState(false); const [guard, setGuard] = useState(800); const [slow, setSlow] = useState(0); const [minVol, setMinVol] = useState(""); @@ -55,7 +57,6 @@ export function CreateMarketCard() { /** What the token button shows: the coin when it was picked, the wrapped token's own name when that was. */ const shown = (picked: string, t?: { symbol: string; onchainSymbol?: string }) => (!t ? undefined : isNative(picked) ? nat.symbol : isWrappedNative(chainId, picked) ? onchainSymbol(t) : t.symbol); const nativeBal = useNativeBalance(chainId); - const digits = priceDigits(grid); const base = grid?.batchInterval ?? deployment?.batchInterval ?? 1800; const period = grid?.period ?? deployment?.gridPeriod ?? 3600; const expiries = now ? gridExpiries(grid?.anchor ?? deployment?.gridAnchor ?? 0, period, now, Math.min(grid?.maxExpiries ?? 8, 8)) : []; @@ -64,13 +65,17 @@ export function CreateMarketCard() { if (!expiry && firstExpiry) setExpiry(firstExpiry); }, [expiry, firstExpiry]); const bb = Q ? parseAmount(bbStr, Q.decimals) : 0n; - const bbOk = bb > 0n && isGridStrike(bb, digits); - const near = Q && bb > 0n ? nearestGridStrikes(bb, digits) : undefined; + const bbOk = !!Q && bb > 0n && isLogGridStrike(bb, Q.decimals); + const near = Q && bb > 0n ? nearestGridStrikes(bb, Q.decimals) : undefined; + const std = A && Q ? standardBand(A.symbol, Q.symbol) : 800; + useEffect(() => { + if (!guardTouched) setGuard(std); + }, [std, guardTouched]); const loV = Q ? parseAmount(lo, Q.decimals) : 0n; const hiV = Q ? parseAmount(hi, Q.decimals) : 0n; const minV = A ? parseAmount(minVol, A.decimals) : 0n; const tooSlow = (c: number) => base * cadenceMultiplier(c) > period; - const m = A && Q && expiry && bbOk ? { asset: A.address, quote: Q.address, expiry, buyBack: bb, kind, feeBps: fee, guardBps: guard, slow, minVolume: minV, firstLow: loV, firstHigh: hiV } : undefined; + const m = A && Q && expiry && bbOk ? { asset: A.address, quote: Q.address, expiry, buyBack: bb, kind, feeBps: fee, guardBps: guard, autoSell, slow, minVolume: minV, firstLow: loV, firstHigh: hiV } : undefined; const pid = m ? poolId(marketKey(m)) : undefined; const exists = (usePoolStatus(pid).data ?? 0) !== 0; const bal = useBalances(useMemo(() => [...new Set([...known, ...(A ? [A.address] : []), ...(Q ? [Q.address] : [])].map((a) => a.toLowerCase()))] as Address[], [known, A, Q])).data; @@ -137,7 +142,7 @@ export function CreateMarketCard() { onChange={setBb} decimals={Q?.decimals ?? 18} token={} - error={bb > 0n && !bbOk ? `Use at most ${digits} significant digits` : undefined} + error={bb > 0n && !bbOk ? "Not on the 1% price grid" : undefined} /> {near && Q && (

@@ -152,10 +157,17 @@ export function CreateMarketCard() {

)} - ({ value: f.bps, label: f.label }))} /> +

Fees are fixed for every market: borrowers pay 5% of the interest plus 0.015% of the amount traded; lenders pay 0.015%.

+
- ({ value: b.bps, label: `±${b.bps / 100}%` }))} /> -

{GUARD_NAME[guard]}. At-market orders only fill this close to last traded.

+ { setGuardTouched(true); setGuard(g); }} options={BAND_TIERS.map((b) => ({ value: b.bps, label: `±${b.bps / 100}%` }))} /> +

{GUARD_NAME[guard]}. At-market orders only fill this close to last traded.{guard === std ? " Standard for this pair: listed on Borrow and Lend." : " Not the standard range for this pair: the market shows only under Markets."}

@@ -168,14 +180,14 @@ export function CreateMarketCard() {

- One market per set of settings; the first creator sets the fee and the market-order price range. New markets are not vault-eligible until they have traded cleanly for four maturities, and the guardian may pause them for up to 7 days at a time. + One market per set of settings; the first creator sets the market-order price range and auto-sell. New markets are not vault-eligible until they have traded cleanly for four maturities, and the guardian may pause them for up to 7 days at a time.

{exists &&

A market with these exact settings already exists.

} {A && Q && !hasUnits &&

You need a tiny amount of both tokens: the contract checks that each one transfers normally.

} diff --git a/apps/web/src/components/more/HowItWorks.tsx b/apps/web/src/components/more/HowItWorks.tsx index 432805e..4db869d 100644 --- a/apps/web/src/components/more/HowItWorks.tsx +++ b/apps/web/src/components/more/HowItWorks.tsx @@ -3,6 +3,8 @@ /** How it works (More): what happens when you borrow, when you lend, and what never happens. Plain words only. */ import Link from "next/link"; import { Card } from "../ui"; +import { ANVIL, ENABLED, deploymentOf } from "@/lib/chains"; +import { networkName } from "@/lib/vocab"; /** One sentence, used verbatim on Borrow and here. */ export const PROMISE = "Fixed-term borrowing against your tokens. Post collateral, receive the loan now, buy the collateral back by the deadline, or keep the proceeds."; @@ -26,19 +28,34 @@ const DIFFERENT = [ const LIMITS = [ "Once a position starts it is never closed early. A roll-over to the next maturity can fail to fill; the current position then stays as it is.", "In markets where you can repay any time, lenders may be paid back before the date.", - "Vault withdrawals can include borrowers' tokens from loans that were not repaid.", + "Vault withdrawals are paid in one token. A vault only lends into markets that sell unrepaid collateral into the next maturity, so a withdrawal can wait for that sale.", "Every loan of the same date shares one repayment window, counted in blocks after the date. A pause of the network pauses it too.", "If a token's issuer can freeze transfers, repaying, claiming and withdrawing that token stop while it is frozen.", - "Anyone can open a market. New markets are marked, vaults don't lend there yet, and a guardian may pause them for up to 7 days at a time.", + "Anyone can open a market. New markets are marked, vaults don't lend there yet, and a guardian may pause them for up to 7 days in total.", ]; const SECURITY = [ "Contracts are immutable: no owner, no upgrade, no admin keys.", - "Only privilege: a guardian may pause a new market for up to 7 days. It cannot move funds.", + "Only privilege: a guardian may pause a new market for up to 7 days in total. It cannot move funds.", "Internal audit, October 2026 (docs/AUDIT_2026-10-11.md on GitHub). No external audit yet.", - "Testnet deployment on Monad Testnet and HyperEVM Testnet, same code at the same address. Test tokens have no value.", + deploymentLine(), ]; +/** + * Where it runs, from the deployments this build has: "Testnet deployment on Monad Testnet and HyperEVM Testnet, same + * code at the same address." plus a served network that is not deployed yet (Sepolia before its deploy). + */ +export function deploymentLine(): string { + const nets = ENABLED.filter((c) => c.id !== ANVIL); + const live = nets.filter((c) => !!deploymentOf(c.id)); + const later = nets.filter((c) => !deploymentOf(c.id)); + const list = (xs: typeof nets) => xs.map(networkName).join(xs.length > 2 ? ", " : " and ").replace(/, ([^,]+)$/, " and $1"); + const cores = new Set(live.map((c) => deploymentOf(c.id)!.confluere.toLowerCase())); + const where = live.length === 0 ? "" : `Testnet deployment on ${list(live)}${live.length > 1 ? (cores.size === 1 ? ", same code at the same address" : ", same code") : ""}. `; + const soon = later.length === 0 ? "" : `${list(later)} ${later.length > 1 ? "are" : "is"} supported and not deployed yet. `; + return `${where}${soon}Test tokens have no value.`; +} + export function HowItWorks() { return (
@@ -79,10 +96,22 @@ export function HowItWorks() {

When you lend

- You buy lender units below the buy-back price. If the borrower repays, you are paid the buy-back price, which is your fixed rate to maturity (minus the protocol's share of the interest). If not, you receive the collateral instead. A vault + You buy lender units below the buy-back price. If the borrower repays, you are paid the buy-back price, which is your fixed rate to maturity. If not, you receive the collateral instead. A vault lends for you by a rule fixed at deployment.

+
+

Fees

+
    +
  • Every trade pays 0.015% of its value on each side (0.03% in all). It goes to the protocol, repaid or not.
  • +
  • + 5% of the interest (buy-back price minus the round's price) is taken from what the borrower receives and held in the market. When the repayment window + closes it is split by the share that was repaid: that part goes to the protocol, the rest goes to lenders, who kept the collateral instead. +
  • +
  • Repaying costs nothing beyond the buy-back price. Quotes on every card already include these fees.
  • +
  • A new market becomes established after four clean maturities in a row, each earning the protocol a fixed minimum in fees.
  • +
+

What never happens