Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/wiki/progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 subgraph/cached RPC length and cutoff race each submitted 1 tx · GREEN: unit 891 incl. helper 5/5 · launch-write 7/7 · lint/typecheck/e2e-ts · helper 72/72 · smoke 63 + 1 skip · visual default+blocked | Dark HOLD: stale auctionLength + render-tick race → reconciled with RPC hydration + handler-time RPC/clock recheck; Engineer review still required for wallet-delay/inclusion semantics and UTC boundary | 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 |
Expand Down
4 changes: 3 additions & 1 deletion e2e/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion e2e/harness/controller.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -34,6 +34,9 @@ class ChainControl {
advance(ms: number): Promise<void> {
return advanceTime(this.page, ms)
}
jumpTo(seconds: number): Promise<void> {
return jumpTime(this.page, seconds)
}
}

// The injected EIP-6963 test wallet (account = TEST_ADDRESS). `chain` reflects
Expand Down
9 changes: 9 additions & 0 deletions e2e/helpers/clock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions e2e/helpers/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ const callOverrides: Record<string, Hex> = {
'*:0x01e1d114': EMPTY_ASSETS, // totalAssets() — registry DTFs get real baskets (chain-state)
'*:0x4d2301cc': ETH_BALANCE, // Multicall3.getEthBalance(address)
'*:0xaa3b5568': IDLE_REBALANCE, // getRebalance() — no active rebalance by default
'*:0x325c25a2': encodeAbiParameters([{ type: 'uint256' }], [1800n]), // auctionLength()
// Fee-display reads on the DTF container (peripheral to governance). A DTF
// with no DAO fee registry reads as fee-free — a valid, deterministic state.
'*:0x9980cb23': ZERO_RETURN, // daoFeeRegistry() → zero address
Expand Down
254 changes: 253 additions & 1 deletion e2e/tests/index-dtf/auctions/launch-write.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,256 @@ 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()
await harness.chain.advance(1_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,
auctionLaunchers: [
...dtfObj.auctionLaunchers,
TEST_ADDRESS.toLowerCase(),
],
},
}
)
overrides.ethCall(
dtf.address,
'0x325c25a2',
encodeAbiParameters([{ type: 'uint256' }], [2n * 60n * 60n])
)
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 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()
await harness.chain.advance(1_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<Record<string, unknown>> }>(
`${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: a non-launcher in the permissionless window submits openAuctionUnrestricted() @smoke', async ({
harness,
overrides,
Expand Down Expand Up @@ -164,7 +414,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 () => {
Expand Down
5 changes: 5 additions & 0 deletions src/locales/en.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
Expand Down
5 changes: 5 additions & 0 deletions src/locales/es.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 5 additions & 0 deletions src/locales/ko.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 시전"
Expand Down
5 changes: 5 additions & 0 deletions src/locales/zh.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 "施放咒语"
Expand Down
12 changes: 9 additions & 3 deletions src/views/index-dtf/auctions/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,15 @@ 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 RPC-read `auctionLength()` reaches
the next UTC day boundary (the Folio TVL fee handout). Launch handlers re-read
`auctionLength()` and the wall clock at invocation, so a governance update or
click between render ticks also fails closed.
ENGINEER REVIEW STILL REQUIRED for
the openAuction weight/price MATH (`getRebalanceOpenAuction`) and the UTC
handout boundary; 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 +
Expand Down
Loading
Loading