diff --git a/docs/wiki/log.md b/docs/wiki/log.md index 844132385..fa6a98dae 100644 --- a/docs/wiki/log.md +++ b/docs/wiki/log.md @@ -1,6 +1,6 @@ --- title: Log -updated: 2026-07-28 +updated: 2026-08-08 type: log --- @@ -125,3 +125,7 @@ Play-by-play lives in git (PRs #1053/#1054/#1055/#1063, SDK PR #27). Durable out - Production chain cycling was a multi-tab feedback loop: each mounted DTF route automatically reasserted its own Ethereum/Base/BSC chain against one wallet-global network. Transaction buttons and the zapper only exposed the changing state. Automatic switching remains, but only the focused, visible document may request it; focusing another DTF tab makes that tab the new chain owner. Index and Yield DTF route contexts share the same guard. - Review caught two smaller bounce paths before closeout: Index initially targeted the lagging global chain atom instead of its provider identity, and cached focus state was not revalidated at the wallet-mutation boundary. Both are now regression-protected; future route-chain synchronization must use the route/provider chain directly. + +## 2026-08-08 + +- DTF Settings confirmation was globally gated by deploy-time schema validity, so unchanged on-chain values with display-rounding drift blocked unrelated governance changes even though those values were omitted from proposal calldata. The gate now scopes errors to changed settings while preserving no-change and changed-invalid guards; the E2E regression models an unchanged invalid distribution plus a valid mandate update. diff --git a/docs/wiki/progress.md b/docs/wiki/progress.md index 930c614c4..3904d65bc 100644 --- a/docs/wiki/progress.md +++ b/docs/wiki/progress.md @@ -1,6 +1,6 @@ --- title: Progress -updated: 2026-07-31 +updated: 2026-08-08 type: ledger --- @@ -10,6 +10,7 @@ Stage ledger. One row per stage; keep entries short. Verifier = exact fresh comm | Stage | Status | Verifier | Review | Next | |---|---|---|---|---| +| 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 | | vlRSR self-appreciating vaults: drawer shares/redeem + rate line · governance card · portfolio · earn rate-corrected + APY | human-review-required (base d0427a7cf; SDK local-linked) | gate green: typecheck+lint+847 unit · 72 helper · smoke 58 · drawer spec 2/2 · live BSC visual light+dark incl. earn TVL 261.5M RSR = totalAssets | Dark + Light on both diffs; all blockers fixed; details in git | PR #1072 open on SDK 0.5.1 (published, pinned exact; direct sdk dep dropped). Companions: dtf-interface#29, reserve-api#236 (deploy w/ daos CDN purge). **Engineer review**: withdraw→redeem for ALL vaults, governance card, api token.price×rate. [plan](../plans/vlrsr-self-appreciating-vaults.md) | | focused-tab automatic chain switching | human-review-required (base b2fcf72c6) | lint/typecheck · unit 840 incl. 8 focus/visibility · helpers 70 · smoke 56 + 1 skipped · wiki-lint | Dark + Light: stale Index target + mutation-boundary focus recheck fixed; automatic switching preserved, background tabs passive | Engineer review wallet/chain flow; then ship | | vote-lock APR unified on /dtf/daos list | done (base 771c92873) | gate-equivalent green (lint/typecheck/unit, 70 helper, 56 smoke, wiki-lint) · live visual: BUILDOUT + POWER overviews and earn all 46.83% from one list request | Dark + Light, per-claim verify — adopted: list-miss/error fallback gating, plain-data return, catalog mixed-case normalization; API re-key REVERTED by Luis (sdk `getVoteLockDao` needs the DTF-address key); accepted: unlisted DTFs keep per-DTF detail cache split | pre-existing reserve-api debt flagged, not shipped: maxAge-before-await caches 500s 24h; unguarded `underlyingPrice.price` deref can 500 whole list | diff --git a/e2e/TEST_MAP.md b/e2e/TEST_MAP.md index 6bb5fe61d..60a24a749 100644 --- a/e2e/TEST_MAP.md +++ b/e2e/TEST_MAP.md @@ -45,7 +45,7 @@ the directory). | Governance – vote-lock drawer | [flows/vote-lock-drawer](tests/flows/vote-lock-drawer.spec.ts) | unlock submits `redeem(shares, account, account)` with previewRedeem-quoted output (self-appreciating rate); lock tab shows previewDeposit shares-out quote | none | no | delegate tab; lock full tx (approve+deposit); reject/revert | | Governance – proposal (view/vote) | [flows/governance-states](tests/flows/governance-states.spec.ts), [flows/governance-multichain](tests/flows/governance-multichain.spec.ts), [flows/governance-permissions](tests/flows/governance-permissions.spec.ts), [flows/governance-support-variants](tests/flows/governance-support-variants.spec.ts), [flows/governance-vote](tests/flows/governance-vote.spec.ts), [flows/governance-queue-execute](tests/flows/governance-queue-execute.spec.ts), [flows/governance-writes-v4](tests/flows/governance-writes-v4.spec.ts), [flows/failures-governance](tests/flows/failures-governance.spec.ts), [flows/governance-description-render](tests/flows/governance-description-render.spec.ts) | PENDING/DEFEATED/QUORUM_NOT_REACHED/EXECUTED/QUEUED states (×chains, v4 governor); For/Against/Abstain vote encode; zero-power/already-voted/window-closed CTA gating; canceller-gated cancel; vote/queue/execute full tx + reject/revert; markdown sanitizer XSS hardening (script/iframe/img-onerror) | none | no | optimistic governance flow (see gaps) | | Governance – create Basket | [flows/governance-propose-basket](tests/flows/governance-propose-basket.spec.ts) | form renders current basket; empty-change guard blocks prepare | none | no | price/liquidity preview, submitted calldata assertion | -| Governance – create DTF Settings | [flows/governance-propose-dtf-settings](tests/flows/governance-propose-dtf-settings.spec.ts) | TVL fee / Mint fee round-trip into setter calldata; no-change keeps confirm disabled | none | no | — | +| Governance – create DTF Settings | [flows/governance-propose-dtf-settings](tests/flows/governance-propose-dtf-settings.spec.ts) | TVL fee / Mint fee round-trip into setter calldata; unchanged invalid distribution state does not block an unrelated mandate proposal; no-change keeps confirm disabled | none | no | — | | Governance – create Basket Settings | [flows/governance-propose-basket-settings](tests/flows/governance-propose-basket-settings.spec.ts), [governance/fee-bounds](tests/index-dtf/governance/fee-bounds.spec.ts) | voting-period round-trips setVotingPeriod calldata (trading governor); single-action guard (no phantom threshold); no-change disabled; out-of-range TVL fee rejected | none | no | — | | Governance – create DAO (Other) | [flows/governance-propose](tests/flows/governance-propose.spec.ts) | DAO-settings proposal full submit flow | none | no | — | | Auctions – rebalance list | [auctions/lifecycle](tests/index-dtf/auctions/lifecycle.spec.ts), [flows/auctions](tests/flows/auctions.spec.ts), [flows/auctions-multichain](tests/flows/auctions-multichain.spec.ts) | list skeleton→list; idle/historical bucketing; auctions-less 0-metrics row; in-window active row (×chains) | partial | yes (lifecycle spec only) | — | diff --git a/e2e/tests/flows/governance-propose-dtf-settings.spec.ts b/e2e/tests/flows/governance-propose-dtf-settings.spec.ts index a4c7610b7..fa99d2867 100644 --- a/e2e/tests/flows/governance-propose-dtf-settings.spec.ts +++ b/e2e/tests/flows/governance-propose-dtf-settings.spec.ts @@ -39,7 +39,12 @@ const SET_MINT_FEE_ABI = parseAbi(['function setMintFee(uint256 _newFee)']) const dtf = findDtfByAddress(DTF_ADDRESS)! type DtfSnapshot = { - dtf: { ownerGovernance: { id: string }; stToken: { id: string } } + dtf: { + mandate: string + feeRecipients: string + ownerGovernance: { id: string } + stToken: { id: string } + } } const loadDtf = () => loadSnapshot(`${dtf.snapshotDir}/dtf.json`).dtf @@ -48,14 +53,18 @@ const UINT_ZERO = encodeAbiParameters([{ type: 'uint256' }], [0n]) // Anchor the frozen clock inside the captured proposal's voting window so every // time-derived read stays consistent with the snapshots. -async function bootProposeFees(page: Page, overrides: MockOverrides) { +async function bootProposeFees( + page: Page, + overrides: MockOverrides, + platformFee: readonly [numerator: bigint, denominator: bigint] = [1n, 5n] +) { const snapshot = loadDtf() // The propose form's revenue seeding is gated on the platform fee resolving // (feeRecipientsAtom returns undefined otherwise). Since B1/M1 a failed // registry read is 'unavailable' — it no longer falls back to a fabricated // 50% that happened to unblock this form — so model a real registry read. - seedFeeRegistry(overrides, dtf, 1n, 5n) // platformFee = 20% + seedFeeRegistry(overrides, dtf, ...platformFee) // bidsEnabled() (0x459cf24b) on the folio is read by the DTF-settings Updater // (v5) but isn't in the central chain-state seed — answer it (inert: we never @@ -118,6 +127,34 @@ const confirmButton = (page: Page) => const submitButton = (page: Page) => page.getByRole('button', { name: 'Submit proposal onchain' }) +test('unchanged distribution rounding drift does not block a mandate change', async ({ + page, + overrides, +}) => { + const snapshot = structuredClone(loadDtf()) + const oneThird = '333333333333333333' + snapshot.feeRecipients = [ + `${snapshot.stToken.id}:${oneThird}`, + `0x280730d9277EF586d58dB74c277Aa710ca8F87C9:${oneThird}`, + ].join(',') + overrides.subgraph({ operationName: 'GetIndexDTF' }, { dtf: snapshot }) + + await bootProposeFees(page, overrides, [1n, 3n]) + + await expect(page.getByTestId('remaining-allocation')).toContainText('22.23%') + await page + .locator('#propose-section-mandate') + .getByRole('button') + .first() + .click() + await advanceTime(page, 1_000) + await page.getByTestId('mandate-input').fill(`${snapshot.mandate} updated`) + await advanceTime(page, 1_000) + + await expect(page.getByTestId('basics-update')).toBeVisible() + await expect(confirmButton(page)).toBeEnabled() +}) + test('TVL fee change: UI percent round-trips into setTVLFee calldata', async ({ page, txLog, diff --git a/src/views/index-dtf/governance/views/propose/views/propose-dtf-settings/atoms.ts b/src/views/index-dtf/governance/views/propose/views/propose-dtf-settings/atoms.ts index c9e66ec17..7a63c4062 100644 --- a/src/views/index-dtf/governance/views/propose/views/propose-dtf-settings/atoms.ts +++ b/src/views/index-dtf/governance/views/propose/views/propose-dtf-settings/atoms.ts @@ -190,9 +190,6 @@ export const currentBasketTokensAtom = atom((get) => { ) }) -// Atom to track form validation state -export const isFormValidAtom = atom(true) - export const isProposalValidAtom = atom((get) => { const removedBasketTokens = get(removedBasketTokensAtom) const hasTokenNameChange = get(hasTokenNameChangeAtom) @@ -207,8 +204,6 @@ export const isProposalValidAtom = atom((get) => { const hasOptimisticGovernanceChanges = get( hasOptimisticGovernanceChangesAtom ) - const isFormValid = get(isFormValidAtom) - const hasChanges = removedBasketTokens.length > 0 || hasTokenNameChange || diff --git a/src/views/index-dtf/governance/views/propose/views/propose-dtf-settings/components/changes/basics-changes.tsx b/src/views/index-dtf/governance/views/propose/views/propose-dtf-settings/components/changes/basics-changes.tsx index f0d18f57c..2f290f5e2 100644 --- a/src/views/index-dtf/governance/views/propose/views/propose-dtf-settings/components/changes/basics-changes.tsx +++ b/src/views/index-dtf/governance/views/propose/views/propose-dtf-settings/components/changes/basics-changes.tsx @@ -38,7 +38,11 @@ const BasicsChanges = () => { return ( Basics Update} + title={ + + Basics Update + + } icon={} > {hasTokenNameChange && isV5 && tokenNameChange && ( diff --git a/src/views/index-dtf/governance/views/propose/views/propose-dtf-settings/components/dtf-settings-proposal-overview.tsx b/src/views/index-dtf/governance/views/propose/views/propose-dtf-settings/components/dtf-settings-proposal-overview.tsx index 39252a82a..7c3845fa5 100644 --- a/src/views/index-dtf/governance/views/propose/views/propose-dtf-settings/components/dtf-settings-proposal-overview.tsx +++ b/src/views/index-dtf/governance/views/propose/views/propose-dtf-settings/components/dtf-settings-proposal-overview.tsx @@ -9,9 +9,7 @@ import { Link } from 'react-router-dom' import { isProposalConfirmedAtom, isProposalValidAtom, - isFormValidAtom, dtfSettingsProposalDataAtom, - proposalDescriptionAtom, } from '../atoms' import DTFSettingsProposalChanges from './dtf-settings-proposal-changes' import SubmitProposalButton from './submit-proposal-button' @@ -19,34 +17,24 @@ import SimulateProposalCard from '@/views/index-dtf/governance/components/simula import { chainIdAtom } from '@/state/atoms' import { Address } from 'viem' import { shouldBypassFormValidation } from '@/utils/form-validation' +import { useFormContext } from 'react-hook-form' +import { useHasRelevantFormErrors } from '../hooks/use-has-relevant-form-errors' -const ConfirmProposalButton = () => { +const ConfirmProposalButton = ({ canProceed }: { canProceed: boolean }) => { const { t } = useLingui() - const isValid = useAtomValue(isProposalValidAtom) - const isFormValid = useAtomValue(isFormValidAtom) const [isProposalConfirmed, setIsProposalConfirmed] = useAtom( isProposalConfirmedAtom ) - const bypassFormValidation = shouldBypassFormValidation() const handleConfirm = () => { - if (!isProposalConfirmed) { - // When confirming, check if form is valid - if (!isFormValid && !bypassFormValidation) { - // The form will show validation errors - return - } - } + if (!isProposalConfirmed && !canProceed) return setIsProposalConfirmed(!isProposalConfirmed) } - // Enable button only if there are changes AND form is valid - const isButtonEnabled = isValid && (isFormValid || bypassFormValidation) - return (