diff --git a/docs/wiki/progress.md b/docs/wiki/progress.md index 9167c77a7..c6dfd8de7 100644 --- a/docs/wiki/progress.md +++ b/docs/wiki/progress.md @@ -10,6 +10,7 @@ Stage ledger. One row per stage; keep entries short. Verifier = exact fresh comm | Stage | Status | Verifier | Review | Next | |---|---|---|---|---| +| Prevent auction overlap with fee handout | human-review-required (base a6f20e340) | RED: stale indexed length, click cutoff, and silent handler block reproduced · GREEN: unit 891 incl. helper 5/5 · launch-write 8/8 · lint/typecheck/e2e-ts · helper 72/72 · smoke 64 + 1 skip · visual default+blocked | Dark HOLD reconciled; PR review comments addressed: indexed render guard + handler RPC/clock recheck, visible click-time feedback, single clock, fake-timer cleanup; Engineer review still required for UTC boundary and wallet/mining safety buffer | PR #1090 open; wiki-lint pre-existing stale design-system blocker | | Preserve Index DTF section in cmd-k navigation | human-review-required (base ebcd6febe) | RED: proposal/rebalance both landed overview; GREEN 2/2 · gate typecheck/lint/880 unit · live Ctrl+K proposal→governance + rebalance→auctions | independent Intent + Engineering Risk PASS, no findings; shared route-selection behavior requires Engineer review | merge only after Engineer review; wiki-lint blocked by pre-existing stale design-system page | | vote modal: address-length title overflowed the dialog | done (base 6854a370b) | lint · typecheck · test:run · e2e helper units · smoke 58 · new `vote-modal-long-title` spec desktop+mobile green · RED-verified (reverted the index-dtf modal fix → checkbox right edge 943 vs dialog 849.9) | product/correctness: self — copy + layout only, no tx path touched | — | | Fix DTF settings confirm button | human-review-required (base 6854a370b) | RED: rounded seeded distribution blocked mandate confirm; GREEN: unit 878 incl. mapper 27/27 · focused E2E 5/5 · typecheck · lint | Dark HOLD on untested mapping → 27 exhaustive mapper tests → Dark PASS; Light PASS; CodeRabbit 2 Minor → resolved (test IDs + editable confirmed state) | PR #1084 open; Engineer review required before merge; wiki-lint blocked by pre-existing stale design-system page | diff --git a/e2e/CLAUDE.md b/e2e/CLAUDE.md index 43fe472df..79f48582f 100644 --- a/e2e/CLAUDE.md +++ b/e2e/CLAUDE.md @@ -116,7 +116,9 @@ failure paths. Receipts resolve only recorded hashes. **Frozen time.** `freezeTime(page, seconds)` BEFORE navigation (browser + Node RPC clocks in lockstep), `advanceTime(page, ms)` after actions. Compute timestamps relative to snapshot data (`proposalTime`, `rebalanceTime`) so -re-captures keep working. +recaptures keep working. Use `harness.chain.jumpTo(seconds)` only to model a +deadline crossing between render timer ticks: it moves `Date.now()` and the RPC +clock without firing timers, so handler-time defenses can be tested directly. **Compliance/geolocation.** `test.use({ compliance: {...} })` sets the API-level geolocation for the whole spec; per-DTF restriction goes through diff --git a/e2e/harness/controller.ts b/e2e/harness/controller.ts index 136030bef..4d7b263b0 100644 --- a/e2e/harness/controller.ts +++ b/e2e/harness/controller.ts @@ -1,6 +1,6 @@ import type { Page } from '@playwright/test' import type { Address, Hex } from 'viem' -import { advanceTime, freezeTime } from '../helpers/clock' +import { advanceTime, freezeTime, jumpTime } from '../helpers/clock' import type { MockOverrides } from '../helpers/overrides' import type { TxRecord } from '../helpers/provider' import type { BoundaryRequest } from '../helpers/requests' @@ -34,6 +34,9 @@ class ChainControl { advance(ms: number): Promise { return advanceTime(this.page, ms) } + jumpTo(seconds: number): Promise { + return jumpTime(this.page, seconds) + } } // The injected EIP-6963 test wallet (account = TEST_ADDRESS). `chain` reflects diff --git a/e2e/helpers/clock.ts b/e2e/helpers/clock.ts index 6b196fe15..cac7a2f7e 100644 --- a/e2e/helpers/clock.ts +++ b/e2e/helpers/clock.ts @@ -33,6 +33,15 @@ export async function advanceTime(page: Page, ms: number) { } } +// Move Date.now() and the RPC clock without firing browser timers. This models +// crossing a deadline between React timer ticks so click handlers must recheck +// time instead of trusting their last rendered state. +export async function jumpTime(page: Page, timestampSeconds: number) { + frozenMs = timestampSeconds * 1000 + await page.clock.setSystemTime(frozenMs) + setMockNow(frozenMs) +} + // A timestamp (seconds) placed inside/around a proposal's voting window, read // from snapshot data so the frozen clock lands the proposal in a known phase. export function proposalTime( diff --git a/e2e/tests/index-dtf/auctions/launch-write.spec.ts b/e2e/tests/index-dtf/auctions/launch-write.spec.ts index 80d6f0f23..409b9391d 100644 --- a/e2e/tests/index-dtf/auctions/launch-write.spec.ts +++ b/e2e/tests/index-dtf/auctions/launch-write.spec.ts @@ -46,11 +46,15 @@ const BSC_USDT = '0x55d398326f99059fF775485246999027B3197955' // Detail-page fills shared by both launch paths: empty token list → 'medium' // volatility; empty liquidity → no warnings; connected wallet's BSC-USDT (a // cmc20 basket token) balanceOf → 0. COVERAGE DEBT: bsc-connected specs need -// central basket balanceOf seeding once they grow beyond this. -function seedAuctionDetail(overrides: { - api: (m: { method?: string; pathname: string }, data: unknown) => unknown - ethCall: (a: string, c: string, r: `0x${string}`) => unknown -}) { +// central basket balanceOf seeding once they grow beyond this. The Folio RPC +// auction length defaults to 30m here and can be changed per scenario. +function seedAuctionDetail( + overrides: { + api: (m: { method?: string; pathname: string }, data: unknown) => unknown + ethCall: (a: string, c: string, r: `0x${string}`) => unknown + }, + auctionLength = 30 * 60 +) { overrides.api({ pathname: '/zapper/tokens' }, []) overrides.api({ method: 'POST', pathname: '/rebalance/liquidity' }, { market: null, @@ -66,6 +70,11 @@ function seedAuctionDetail(overrides: { }), encodeAbiParameters([{ type: 'uint256' }], [0n]) ) + overrides.ethCall( + dtf.address, + '0x325c25a2', + encodeAbiParameters([{ type: 'uint256' }], [BigInt(auctionLength)]) + ) } test('auctions: an auction launcher submits openAuction() to the folio @smoke', async ({ @@ -124,6 +133,334 @@ test('auctions: an auction launcher submits openAuction() to the folio @smoke', expect(decoded.args[0]).toBe(BigInt(latest.nonce)) }) +test('auctions: blocks launcher auctions that would overlap the daily TVL fee handout @smoke', async ({ + harness, + overrides, +}) => { + const page = harness.page + const latest = loadRebalances(dtf)[0] + const { dtf: dtfObj } = loadSnapshot<{ + dtf: { auctionLaunchers: string[] } + }>(`${dtf.snapshotDir}/dtf.json`) + const nextFeeHandout = + (Math.floor(Number(latest.timestamp) / (24 * 60 * 60)) + 1) * 24 * 60 * 60 + + await harness.chain.freezeAt(nextFeeHandout - 30 * 60) + overrides.subgraph( + { operationName: 'GetIndexDTF' }, + { + dtf: { + ...dtfObj, + auctionLaunchers: [ + ...dtfObj.auctionLaunchers, + TEST_ADDRESS.toLowerCase(), + ], + }, + } + ) + seedAuctionDetail(overrides) + overrides.ethCall( + dtf.address, + '0xaa3b5568', + encodeActiveRebalance(dtf, latest) + ) + + await harness.goto(dtf, `auctions/rebalance/${proposalIdFor(dtf, latest)}`) + await harness.wallet.connect() + await expect(page.getByTestId('dtf-auctions')).toBeVisible({ + timeout: 20_000, + }) + + const launch = page.getByTestId('auctions-launch-btn') + await expect(async () => { + await harness.chain.advance(5_000) + await expect(launch).toBeVisible() + await expect(launch).toBeDisabled() + await expect(page.getByTestId('auctions-fee-handout-overlap')).toBeVisible() + }).toPass({ timeout: 30_000 }) + + expect(harness.tx.log).toHaveLength(0) +}) + +test('auctions: rechecks the fee cutoff at click time before the next render @smoke', async ({ + harness, + overrides, +}) => { + const page = harness.page + const latest = loadRebalances(dtf)[0] + const { dtf: dtfObj } = loadSnapshot<{ + dtf: { auctionLaunchers: string[] } + }>(`${dtf.snapshotDir}/dtf.json`) + const nextFeeHandout = + (Math.floor(Number(latest.timestamp) / (24 * 60 * 60)) + 1) * 24 * 60 * 60 + const cutoff = nextFeeHandout - 30 - 30 * 60 + + await harness.chain.freezeAt(cutoff - 30) + overrides.subgraph( + { operationName: 'GetIndexDTF' }, + { + dtf: { + ...dtfObj, + auctionLaunchers: [ + ...dtfObj.auctionLaunchers, + TEST_ADDRESS.toLowerCase(), + ], + }, + } + ) + seedAuctionDetail(overrides) + overrides.ethCall( + dtf.address, + '0xaa3b5568', + encodeActiveRebalance(dtf, latest) + ) + + await harness.goto(dtf, `auctions/rebalance/${proposalIdFor(dtf, latest)}`) + await harness.wallet.connect() + await expect(page.getByTestId('dtf-auctions')).toBeVisible({ + timeout: 20_000, + }) + + const launch = page.getByTestId('auctions-launch-btn') + await expect(async () => { + await harness.chain.advance(5_000) + await expect(launch).toBeEnabled() + }).toPass({ timeout: 30_000 }) + + await harness.chain.jumpTo(cutoff) + await expect(launch).toBeEnabled() + await launch.click() + + const overlapToast = page.locator('[data-sonner-toast][data-type="error"]') + await expect(async () => { + await harness.chain.advance(1_000) + await expect(overlapToast).toContainText( + 'Cannot launch: auction would overlap the daily TVL fee handout', + { timeout: 1_000 } + ) + }).toPass({ timeout: 15_000 }) + expect(harness.tx.log).toHaveLength(0) +}) + +test('auctions: uses live RPC auction length when the subgraph is stale @smoke', async ({ + harness, + overrides, +}) => { + const page = harness.page + const latest = loadRebalances(dtf)[0] + const { dtf: dtfObj } = loadSnapshot<{ + dtf: { auctionLaunchers: string[]; auctionLength: string } + }>(`${dtf.snapshotDir}/dtf.json`) + const nextFeeHandout = + (Math.floor(Number(latest.timestamp) / (24 * 60 * 60)) + 1) * 24 * 60 * 60 + + await harness.chain.freezeAt(nextFeeHandout - 60 * 60) + overrides.subgraph( + { operationName: 'GetIndexDTF' }, + { + dtf: { + ...dtfObj, + auctionLength: String(30 * 60), + auctionLaunchers: [ + ...dtfObj.auctionLaunchers, + TEST_ADDRESS.toLowerCase(), + ], + }, + } + ) + seedAuctionDetail(overrides, 2 * 60 * 60) + overrides.ethCall( + dtf.address, + '0xaa3b5568', + encodeActiveRebalance(dtf, latest) + ) + + await harness.goto(dtf, `auctions/rebalance/${proposalIdFor(dtf, latest)}`) + await harness.wallet.connect() + await expect(page.getByTestId('dtf-auctions')).toBeVisible({ + timeout: 20_000, + }) + + const launch = page.getByTestId('auctions-launch-btn') + await expect(async () => { + await harness.chain.advance(5_000) + await expect(launch).toBeVisible() + await expect(launch).toBeEnabled() + }).toPass({ timeout: 30_000 }) + + await launch.click() + + const overlapToast = page.locator('[data-sonner-toast][data-type="error"]') + await expect(async () => { + await harness.chain.advance(1_000) + await expect(overlapToast).toContainText( + 'Cannot launch: auction would overlap the daily TVL fee handout', + { timeout: 1_000 } + ) + }).toPass({ timeout: 15_000 }) + expect(harness.tx.log).toHaveLength(0) +}) + +test('auctions: rechecks live RPC auction length at click time @smoke', async ({ + harness, + overrides, +}) => { + const page = harness.page + const latest = loadRebalances(dtf)[0] + const { dtf: dtfObj } = loadSnapshot<{ + dtf: { auctionLaunchers: string[] } + }>(`${dtf.snapshotDir}/dtf.json`) + const nextFeeHandout = + (Math.floor(Number(latest.timestamp) / (24 * 60 * 60)) + 1) * 24 * 60 * 60 + + await harness.chain.freezeAt(nextFeeHandout - 60 * 60) + overrides.subgraph( + { operationName: 'GetIndexDTF' }, + { + dtf: { + ...dtfObj, + auctionLaunchers: [...dtfObj.auctionLaunchers, TEST_ADDRESS.toLowerCase()], + }, + } + ) + seedAuctionDetail(overrides) + overrides.ethCall(dtf.address, '0xaa3b5568', encodeActiveRebalance(dtf, latest)) + + await harness.goto(dtf, `auctions/rebalance/${proposalIdFor(dtf, latest)}`) + await harness.wallet.connect() + await expect(page.getByTestId('dtf-auctions')).toBeVisible({ timeout: 20_000 }) + + const launch = page.getByTestId('auctions-launch-btn') + await expect(async () => { + await harness.chain.advance(5_000) + await expect(launch).toBeEnabled() + }).toPass({ timeout: 30_000 }) + + overrides.ethCall( + dtf.address, + '0x325c25a2', + encodeAbiParameters([{ type: 'uint256' }], [2n * 60n * 60n]) + ) + await launch.click() + + const overlapToast = page.locator('[data-sonner-toast][data-type="error"]') + await expect(async () => { + await harness.chain.advance(1_000) + await expect(overlapToast).toContainText( + 'Cannot launch: auction would overlap the daily TVL fee handout', + { timeout: 1_000 } + ) + }).toPass({ timeout: 15_000 }) + expect(harness.tx.log).toHaveLength(0) +}) + +test('auctions: blocks community auctions that would overlap the daily TVL fee handout @smoke', async ({ + harness, + overrides, +}) => { + const page = harness.page + const raw = loadRebalances(dtf)[0] + const nextFeeHandout = + (Math.floor(Number(raw.restrictedUntil) / (24 * 60 * 60)) + 1) * + 24 * + 60 * + 60 + const availableUntil = String(nextFeeHandout + 60 * 60) + const permissionless = { ...raw, availableUntil } + + await harness.chain.freezeAt(nextFeeHandout - 30 * 60) + const rebSnap = loadSnapshot<{ rebalances: Array> }>( + `${dtf.snapshotDir}/rebalances.json` + ) + overrides.subgraph( + { operationName: 'getRebalances' }, + { + rebalances: rebSnap.rebalances.map((r) => + r.blockNumber === raw.blockNumber ? { ...r, availableUntil } : r + ), + } + ) + seedAuctionDetail(overrides) + overrides.ethCall( + dtf.address, + '0xaa3b5568', + encodeActiveRebalance(dtf, permissionless) + ) + + await harness.goto(dtf, `auctions/rebalance/${proposalIdFor(dtf, raw)}`) + await harness.wallet.connect() + await expect(page.getByTestId('dtf-auctions')).toBeVisible({ + timeout: 20_000, + }) + + const launch = page.getByTestId('auctions-community-launch-btn') + await expect(async () => { + await harness.chain.advance(5_000) + await expect(launch).toBeVisible() + await expect(launch).toBeDisabled() + await expect(page.getByTestId('auctions-fee-handout-overlap')).toBeVisible() + }).toPass({ timeout: 30_000 }) + expect(harness.tx.log).toHaveLength(0) +}) + +test('auctions: community launch surfaces a click-time fee cutoff block @smoke', async ({ + harness, + overrides, +}) => { + const page = harness.page + const raw = loadRebalances(dtf)[0] + const nextFeeHandout = + (Math.floor(Number(raw.restrictedUntil) / (24 * 60 * 60)) + 1) * + 24 * + 60 * + 60 + const auctionLength = 30 * 60 + const cutoff = nextFeeHandout - 30 - auctionLength + const availableUntil = String(nextFeeHandout + 60 * 60) + const permissionless = { ...raw, availableUntil } + + await harness.chain.freezeAt(cutoff - 30) + const rebSnap = loadSnapshot<{ rebalances: Array> }>( + `${dtf.snapshotDir}/rebalances.json` + ) + overrides.subgraph( + { operationName: 'getRebalances' }, + { + rebalances: rebSnap.rebalances.map((r) => + r.blockNumber === raw.blockNumber ? { ...r, availableUntil } : r + ), + } + ) + seedAuctionDetail(overrides, auctionLength) + overrides.ethCall( + dtf.address, + '0xaa3b5568', + encodeActiveRebalance(dtf, permissionless) + ) + + await harness.goto(dtf, `auctions/rebalance/${proposalIdFor(dtf, raw)}`) + await harness.wallet.connect() + await expect(page.getByTestId('dtf-auctions')).toBeVisible({ + timeout: 20_000, + }) + + const launch = page.getByTestId('auctions-community-launch-btn') + await expect(async () => { + await harness.chain.advance(5_000) + await expect(launch).toBeEnabled() + }).toPass({ timeout: 30_000 }) + + await harness.chain.jumpTo(cutoff) + await expect(launch).toBeEnabled() + await launch.click() + await harness.chain.advance(1_000) + + await expect(page.getByTestId('auctions-launch-error')).toContainText( + 'Cannot launch: auction would overlap the daily TVL fee handout' + ) + expect(harness.tx.log).toHaveLength(0) +}) + test('auctions: a non-launcher in the permissionless window submits openAuctionUnrestricted() @smoke', async ({ harness, overrides, @@ -164,7 +501,9 @@ test('auctions: a non-launcher in the permissionless window submits openAuctionU await harness.goto(dtf, `auctions/rebalance/${proposalIdFor(dtf, raw)}`) await harness.wallet.connect() - await expect(page.getByTestId('dtf-auctions')).toBeVisible({ timeout: 20_000 }) + await expect(page.getByTestId('dtf-auctions')).toBeVisible({ + timeout: 20_000, + }) const launch = page.getByTestId('auctions-community-launch-btn') await expect(async () => { diff --git a/src/locales/en.po b/src/locales/en.po index 2d90cb46d..5420b792b 100644 --- a/src/locales/en.po +++ b/src/locales/en.po @@ -2121,6 +2121,11 @@ msgstr "" msgid "Candlestick chart" msgstr "" +#: src/views/index-dtf/auctions/views/rebalance/components/community-launch-auctions-button.tsx:159 +#: src/views/index-dtf/auctions/views/rebalance/components/launch-auctions-button.tsx:198 +msgid "Cannot launch: auction would overlap the daily TVL fee handout" +msgstr "" + #: src/views/yield-dtf/governance/views/proposal/components/SpellUpgrade4_2_0.tsx:79 msgid "Cast Spell" msgstr "" diff --git a/src/locales/es.po b/src/locales/es.po index 4d84fcbf7..5c785a50c 100644 --- a/src/locales/es.po +++ b/src/locales/es.po @@ -2121,6 +2121,11 @@ msgstr "Velas" msgid "Candlestick chart" msgstr "Gráfico de velas" +#: src/views/index-dtf/auctions/views/rebalance/components/community-launch-auctions-button.tsx:159 +#: src/views/index-dtf/auctions/views/rebalance/components/launch-auctions-button.tsx:198 +msgid "Cannot launch: auction would overlap the daily TVL fee handout" +msgstr "No se puede iniciar: la subasta se solaparía con la distribución diaria de la comisión por TVL" + #: src/views/yield-dtf/governance/views/proposal/components/SpellUpgrade4_2_0.tsx:79 msgid "Cast Spell" msgstr "Lanzar Spell" diff --git a/src/locales/ko.po b/src/locales/ko.po index e590371c5..d11aeb706 100644 --- a/src/locales/ko.po +++ b/src/locales/ko.po @@ -2121,6 +2121,11 @@ msgstr "캔들" msgid "Candlestick chart" msgstr "캔들 차트" +#: src/views/index-dtf/auctions/views/rebalance/components/community-launch-auctions-button.tsx:159 +#: src/views/index-dtf/auctions/views/rebalance/components/launch-auctions-button.tsx:198 +msgid "Cannot launch: auction would overlap the daily TVL fee handout" +msgstr "시작할 수 없음: 경매가 일일 TVL 수수료 분배와 겹칩니다" + #: src/views/yield-dtf/governance/views/proposal/components/SpellUpgrade4_2_0.tsx:79 msgid "Cast Spell" msgstr "Spell 시전" diff --git a/src/locales/zh.po b/src/locales/zh.po index 841e7565b..a1d04f38c 100644 --- a/src/locales/zh.po +++ b/src/locales/zh.po @@ -2121,6 +2121,11 @@ msgstr "蜡烛图" msgid "Candlestick chart" msgstr "K线图" +#: src/views/index-dtf/auctions/views/rebalance/components/community-launch-auctions-button.tsx:159 +#: src/views/index-dtf/auctions/views/rebalance/components/launch-auctions-button.tsx:198 +msgid "Cannot launch: auction would overlap the daily TVL fee handout" +msgstr "无法启动:拍卖将与每日 TVL 费用分发时间重叠" + #: src/views/yield-dtf/governance/views/proposal/components/SpellUpgrade4_2_0.tsx:79 msgid "Cast Spell" msgstr "施放咒语" diff --git a/src/views/index-dtf/auctions/CLAUDE.md b/src/views/index-dtf/auctions/CLAUDE.md index 02ab49ac9..10201e432 100644 --- a/src/views/index-dtf/auctions/CLAUDE.md +++ b/src/views/index-dtf/auctions/CLAUDE.md @@ -76,9 +76,17 @@ Quick loop: `pnpm exec playwright test e2e/tests/smoke/auctions.spec.ts` the D1 weightControl derivation was reverted, Luis 2026-07-21) — cmc20 is tracking so it stays non-hybrid; a hybrid (allowlisted native) DTF forces a Manage-Weights step before the launch button, so mock - `weightControl` to drive that step. ENGINEER REVIEW STILL REQUIRED for the openAuction - weight/price MATH (`getRebalanceOpenAuction`) — the spec proves the call fires, - not that the args are numerically correct. + `weightControl` to drive that step. Both launcher and community paths are also + blocked when the 30s auction warmup plus the indexed DTF `auctionLength` + reaches the next UTC day boundary (the Folio TVL fee handout). Launch handlers + re-read `auctionLength()` from RPC and recheck the wall clock at invocation, + so a stale index, governance update, or click between render ticks fails + closed. If the handler blocks an apparently enabled button, it surfaces the + translated overlap reason instead of silently returning. + ENGINEER REVIEW STILL REQUIRED for the openAuction weight/price MATH + (`getRebalanceOpenAuction`), the UTC handout boundary, and whether the + wallet-confirmation/mining delay requires a safety buffer; the smoke proves + control-flow wiring, not economic correctness. - **Deferred** (needs testids/roles + engineer review): `bid` writes; legacy v2 UI and `/auctions/legacy` route. - **Covered** (`flows/auctions-multichain.spec.ts`): historical bucketing + diff --git a/src/views/index-dtf/auctions/views/rebalance/components/community-launch-auctions-button.tsx b/src/views/index-dtf/auctions/views/rebalance/components/community-launch-auctions-button.tsx index f07525ec4..da8d707d5 100644 --- a/src/views/index-dtf/auctions/views/rebalance/components/community-launch-auctions-button.tsx +++ b/src/views/index-dtf/auctions/views/rebalance/components/community-launch-auctions-button.tsx @@ -6,7 +6,11 @@ import { Trans, useLingui } from '@lingui/react/macro' import { atom, useAtom, useAtomValue } from 'jotai' import { LoaderCircle, MousePointerBan } from 'lucide-react' import { useEffect, useState } from 'react' -import { useWaitForTransactionReceipt, useWriteContract } from 'wagmi' +import { + usePublicClient, + useWaitForTransactionReceipt, + useWriteContract, +} from 'wagmi' import { currentRebalanceAtom } from '../../../atoms' import { isAuctionOngoingAtom, @@ -16,6 +20,11 @@ import { } from '../atoms' import useRebalanceParams from '../hooks/use-rebalance-params' import Help from '@/components/ui/help' +import useCurrentTime from '@/hooks/useCurrentTime' +import { + wouldAuctionOverlapFeeHandout, + wouldAuctionOverlapFeeHandoutNow, +} from '../utils/auction-fee-handout-overlap' const auctionNumberAtom = atom((get) => { const auctions = get(rebalanceAuctionsAtom) @@ -32,42 +41,30 @@ const CommunityLaunchAuctionsButton = () => { const auctionNumber = useAtomValue(auctionNumberAtom) const [isLaunching, setIsLaunching] = useState(false) const { writeContract, isError, isPending, data } = useWriteContract() + const publicClient = usePublicClient({ chainId: dtf?.chainId }) const { isSuccess } = useWaitForTransactionReceipt({ hash: data, chainId: dtf?.chainId, }) const [error, setError] = useState(null) - const [countdown, setCountdown] = useState(0) const isAuctionOngoing = useAtomValue(isAuctionOngoingAtom) - const currentTime = Math.floor(Date.now() / 1000) + const currentTime = useCurrentTime() const restrictedUntil = rebalance ? Number(rebalance.rebalance.restrictedUntil) : 0 - const isRestrictedPeriod = rebalance && restrictedUntil > currentTime - const timeUntilPermissionless = isRestrictedPeriod - ? restrictedUntil - currentTime - : 0 - const isValid = !!rebalanceParams && rebalancePercent > 0 && rebalance && dtf + const isRestrictedPeriod = !!rebalance && restrictedUntil > currentTime + const countdown = Math.max(0, restrictedUntil - currentTime) + const isFeeHandoutOverlap = + !!dtf && wouldAuctionOverlapFeeHandout(currentTime, dtf.auctionLength) + const isValid = + !!rebalanceParams && + rebalancePercent > 0 && + rebalance && + dtf && + !isFeeHandoutOverlap const isNotCommunityLaunch = rebalance?.rebalance.availableUntil === rebalance?.rebalance.restrictedUntil - // Countdown effect for restricted period - useEffect(() => { - if (isRestrictedPeriod) { - const interval = setInterval(() => { - const newTime = Math.floor(Date.now() / 1000) - const remaining = restrictedUntil - newTime - setCountdown(Math.max(0, remaining)) - - if (remaining <= 0) { - clearInterval(interval) - } - }, 1000) - - return () => clearInterval(interval) - } - }, [isRestrictedPeriod, restrictedUntil]) - useEffect(() => { if (isSuccess) { setError(null) @@ -87,15 +84,36 @@ const CommunityLaunchAuctionsButton = () => { } }, [isSuccess]) - const handleStartAuctions = () => { - if (!isValid || !rebalanceParams) return + const handleStartAuctions = async () => { + if ( + !isValid || + !dtf || + !publicClient || + !rebalanceParams + ) + return try { setIsLaunching(true) setError(null) + const auctionLength = Number( + await publicClient.readContract({ + address: dtf.id, + abi: dtfIndexAbi, + functionName: 'auctionLength', + }) + ) + if (wouldAuctionOverlapFeeHandoutNow(auctionLength)) { + setIsLaunching(false) + setError( + t`Cannot launch: auction would overlap the daily TVL fee handout` + ) + return + } + writeContract({ - address: dtf?.id, + address: dtf.id, abi: dtfIndexAbi, functionName: 'openAuctionUnrestricted', args: [BigInt(rebalance.rebalance.nonce)], @@ -119,7 +137,7 @@ const CommunityLaunchAuctionsButton = () => { ) } - if (isRestrictedPeriod && timeUntilPermissionless > 0) { + if (isRestrictedPeriod && countdown > 0) { return (
- {error &&
{error}
} + {error && ( +
+ {error} +
+ )}
) } diff --git a/src/views/index-dtf/auctions/views/rebalance/components/launch-auctions-button.tsx b/src/views/index-dtf/auctions/views/rebalance/components/launch-auctions-button.tsx index cc2139cae..e877386c1 100644 --- a/src/views/index-dtf/auctions/views/rebalance/components/launch-auctions-button.tsx +++ b/src/views/index-dtf/auctions/views/rebalance/components/launch-auctions-button.tsx @@ -8,7 +8,11 @@ import { LoaderCircle } from 'lucide-react' import { useEffect, useMemo, useState } from 'react' import { toast } from 'sonner' import { Address } from 'viem' -import { useWaitForTransactionReceipt, useWriteContract } from 'wagmi' +import { + usePublicClient, + useWaitForTransactionReceipt, + useWriteContract, +} from 'wagmi' import { currentRebalanceAtom } from '../../../atoms' import { areWeightsSavedAtom, @@ -26,6 +30,11 @@ import getRebalanceOpenAuction, { buildRebalanceOpenAuctionArrays, } from '../utils/get-rebalance-open-auction' import { TransactionButtonContainer } from '@/components/ui/transaction' +import useCurrentTime from '@/hooks/useCurrentTime' +import { + wouldAuctionOverlapFeeHandout, + wouldAuctionOverlapFeeHandoutNow, +} from '../utils/auction-fee-handout-overlap' const auctionNumberAtom = atom((get) => { const auctions = get(rebalanceAuctionsAtom) @@ -44,6 +53,7 @@ const LaunchAuctionsButton = () => { const auctionNumber = useAtomValue(auctionNumberAtom) const [isLaunching, setIsLaunching] = useState(false) const { writeContract, isError, isPending, data } = useWriteContract() + const publicClient = usePublicClient({ chainId: dtf?.chainId }) const { isSuccess } = useWaitForTransactionReceipt({ hash: data, chainId: dtf?.chainId, @@ -53,6 +63,9 @@ const LaunchAuctionsButton = () => { const savedWeights = useAtomValue(savedWeightsAtom) const areWeightsSaved = useAtomValue(areWeightsSavedAtom) const auctions = useAtomValue(rebalanceAuctionsAtom) + const currentTime = useCurrentTime() + const isFeeHandoutOverlap = + !!dtf && wouldAuctionOverlapFeeHandout(currentTime, dtf.auctionLength) const weightsToUse = isHybridDTF && areWeightsSaved && savedWeights && auctions.length === 0 @@ -91,7 +104,8 @@ const LaunchAuctionsButton = () => { rebalancePercent > 0 && rebalance && dtf && - !priceUnavailable + !priceUnavailable && + !isFeeHandoutOverlap useEffect(() => { if (isSuccess) { @@ -119,12 +133,34 @@ const LaunchAuctionsButton = () => { } }, [isError]) - const handleStartAuctions = () => { - if (!isValid || !rebalanceParams || !weightsToUse) return + const handleStartAuctions = async () => { + if ( + !isValid || + !dtf || + !publicClient || + !rebalanceParams || + !weightsToUse + ) + return try { setIsLaunching(true) + const auctionLength = Number( + await publicClient.readContract({ + address: dtf.id, + abi: dtfIndexAbi, + functionName: 'auctionLength', + }) + ) + if (wouldAuctionOverlapFeeHandoutNow(auctionLength)) { + setIsLaunching(false) + toast.error( + t`Cannot launch: auction would overlap the daily TVL fee handout` + ) + return + } + const [openAuctionArgs] = getRebalanceOpenAuction( rebalanceParams.folioVersion, rebalance.rebalance.tokens, @@ -143,7 +179,7 @@ const LaunchAuctionsButton = () => { ) writeContract({ - address: dtf?.id, + address: dtf.id, abi: dtfIndexAbi, functionName: 'openAuction', args: [ @@ -181,6 +217,16 @@ const LaunchAuctionsButton = () => { )}

)} + {isFeeHandoutOverlap && ( +

+ + Cannot launch: auction would overlap the daily TVL fee handout + +

+ )}