Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 5 additions & 1 deletion docs/wiki/log.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Log
updated: 2026-07-28
updated: 2026-08-08
type: log
---

Expand Down Expand Up @@ -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.
3 changes: 2 additions & 1 deletion docs/wiki/progress.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Progress
updated: 2026-07-31
updated: 2026-08-08
type: ledger
---

Expand All @@ -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; no Critical/Important/Minor | open PR; 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 |
Expand Down
2 changes: 1 addition & 1 deletion e2e/TEST_MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) | — |
Expand Down
43 changes: 40 additions & 3 deletions e2e/tests/flows/governance-propose-dtf-settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<DtfSnapshot>(`${dtf.snapshotDir}/dtf.json`).dtf

Expand All @@ -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
Expand Down Expand Up @@ -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.getByText('Remaining allocation: 22.23%')).toBeVisible()
await page
.locator('#propose-section-mandate')
.getByRole('button')
.first()
.click()
await advanceTime(page, 1_000)
await page.getByLabel('Mandate').fill(`${snapshot.mandate} updated`)
await advanceTime(page, 1_000)

await expect(page.getByText('Basics Update')).toBeVisible()
await expect(confirmButton(page)).toBeEnabled()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
})

test('TVL fee change: UI percent round-trips into setTVLFee calldata', async ({
page,
txLog,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -207,8 +204,6 @@ export const isProposalValidAtom = atom((get) => {
const hasOptimisticGovernanceChanges = get(
hasOptimisticGovernanceChangesAtom
)
const isFormValid = get(isFormValidAtom)

const hasChanges =
removedBasketTokens.length > 0 ||
hasTokenNameChange ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,44 +9,32 @@ 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'
import SimulateProposalCard from '@/views/index-dtf/governance/components/simulate-proposal-card'
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 (
<Button
className="w-full"
disabled={!isButtonEnabled}
disabled={!canProceed}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
variant={isProposalConfirmed ? 'outline' : 'default'}
onClick={handleConfirm}
>
Expand All @@ -58,11 +46,14 @@ const ConfirmProposalButton = () => {
const ProposalInstructions = () => {
const { t } = useLingui()
const isValid = useAtomValue(isProposalValidAtom)
const isFormValid = useAtomValue(isFormValidAtom)
const confirmed = useAtomValue(isProposalConfirmedAtom)
const {
formState: { errors },
} = useFormContext()
const hasRelevantFormErrors = useHasRelevantFormErrors(errors)
const bypassFormValidation = shouldBypassFormValidation()

const canProceed = isValid && (isFormValid || bypassFormValidation)
const canProceed = isValid && (!hasRelevantFormErrors || bypassFormValidation)

const timelineItems = [
{
Expand All @@ -72,7 +63,7 @@ const ProposalInstructions = () => {
},
{
title: t`Finalize basket proposal`,
children: <ConfirmProposalButton />,
children: <ConfirmProposalButton canProceed={canProceed} />,
isActive: canProceed && !confirmed,
isCompleted: confirmed,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import type { FieldError, FieldErrors } from 'react-hook-form'
import type { ProposeSettings } from '../form-fields'

type SettingsErrors = FieldErrors<ProposeSettings> & {
optimistic?: FieldError
roles?: FieldError
'revenue-distribution'?: FieldError
}

export type RelevantFormChanges = {
tokenNameChange?: unknown
mandateChange?: unknown
rolesChanges: {
guardians?: unknown
brandManagers?: unknown
auctionLaunchers?: unknown
}
revenueDistributionChanges: {
governanceShare?: unknown
deployerShare?: unknown
additionalRecipients?: unknown
}
dtfRevenueChanges: { mintFee?: unknown; tvlFee?: unknown }
auctionLengthChange?: unknown
weightControlChange?: unknown
bidsEnabledChange?: unknown
governanceChanges: {
votingDelay?: unknown
votingPeriod?: unknown
quorumPercent?: unknown
proposalThreshold?: unknown
executionDelay?: unknown
}
optimisticChanges: {
vetoDelay?: unknown
vetoPeriod?: unknown
vetoThreshold?: unknown
optimisticProposers?: unknown
}
}

// Seeded values can fail deploy-time validation; only changed values enter calldata.
export const hasRelevantFormErrors = (
formErrors: FieldErrors,
changes: RelevantFormChanges
) => {
const errors = formErrors as SettingsErrors
const {
tokenNameChange,
mandateChange,
rolesChanges,
revenueDistributionChanges,
dtfRevenueChanges,
auctionLengthChange,
weightControlChange,
bidsEnabledChange,
governanceChanges,
optimisticChanges,
} = changes
const hasRolesChange =
rolesChanges.guardians !== undefined ||
rolesChanges.brandManagers !== undefined ||
rolesChanges.auctionLaunchers !== undefined
const hasDistributionChange =
revenueDistributionChanges.governanceShare !== undefined ||
revenueDistributionChanges.deployerShare !== undefined ||
revenueDistributionChanges.additionalRecipients !== undefined

return Boolean(
(tokenNameChange !== undefined && errors.tokenName) ||
(mandateChange !== undefined && errors.mandate) ||
(rolesChanges.guardians !== undefined && errors.guardians) ||
(rolesChanges.brandManagers !== undefined && errors.brandManagers) ||
(rolesChanges.auctionLaunchers !== undefined && errors.auctionLaunchers) ||
(hasRolesChange && errors.roles) ||
(revenueDistributionChanges.governanceShare !== undefined &&
errors.governanceShare) ||
(revenueDistributionChanges.deployerShare !== undefined &&
errors.deployerShare) ||
(revenueDistributionChanges.additionalRecipients !== undefined &&
errors.additionalRevenueRecipients) ||
(hasDistributionChange && errors['revenue-distribution']) ||
(dtfRevenueChanges.mintFee !== undefined && errors.mintFee) ||
(dtfRevenueChanges.tvlFee !== undefined && errors.folioFee) ||
(auctionLengthChange !== undefined && errors.auctionLength) ||
(weightControlChange !== undefined && errors.weightControl) ||
(bidsEnabledChange !== undefined && errors.bidsEnabled) ||
(governanceChanges.votingDelay !== undefined &&
errors.governanceVotingDelay) ||
(governanceChanges.votingPeriod !== undefined &&
errors.governanceVotingPeriod) ||
(governanceChanges.quorumPercent !== undefined &&
errors.governanceVotingQuorum) ||
(governanceChanges.proposalThreshold !== undefined &&
errors.governanceVotingThreshold) ||
(governanceChanges.executionDelay !== undefined &&
errors.governanceExecutionDelay) ||
(optimisticChanges.vetoDelay !== undefined && errors.optimisticVetoDelay) ||
(optimisticChanges.vetoPeriod !== undefined &&
errors.optimisticVetoPeriod) ||
(optimisticChanges.vetoThreshold !== undefined &&
errors.optimisticVetoThreshold) ||
(optimisticChanges.optimisticProposers !== undefined &&
(errors.optimisticProposers || errors.optimistic))
)
}
Loading
Loading