Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion src/locales/el/messages.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/locales/en/messages.js

Large diffs are not rendered by default.

25 changes: 21 additions & 4 deletions src/locales/en/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,10 @@ msgstr "Aave is a fully decentralized, community governed protocol by the AAVE t
msgid "Please enter a valid wallet address."
msgstr "Please enter a valid wallet address."

#: src/modules/governance/proposal/ProposalTimeline.tsx
msgid "Payload {payloadId} executed"
msgstr "Payload {payloadId} executed"

#: src/components/ConnectWalletPaper.tsx
msgid "Connect your wallet to see your supplies, borrowings, and open positions."
msgstr "Connect your wallet to see your supplies, borrowings, and open positions."
Expand Down Expand Up @@ -1812,10 +1816,6 @@ msgstr "Email"
msgid "here"
msgstr "here"

#: src/modules/governance/proposal/ProposalTimeline.tsx
msgid "Payload {0} executed"
msgstr "Payload {0} executed"

#: src/modules/staking/StakingPanel.tsx
#: src/modules/umbrella/UnstakeModal.tsx
msgid "Unstake"
Expand Down Expand Up @@ -1974,6 +1974,10 @@ msgstr "The Aave Balancer Pool Token (ABPT) is a liquidity pool token. You can r
msgid "Merit Rewards"
msgstr "Merit Rewards"

#: src/modules/governance/proposal/ProposalTimeline.tsx
msgid "Payload {payloadId} execution"
msgstr "Payload {payloadId} execution"

#: src/components/transactions/SGhoVault/SGhoVaultWithdrawActions.tsx
#: src/components/transactions/Withdraw/WithdrawAndUnwrapActions.tsx
msgid "Withdrawing"
Expand Down Expand Up @@ -3308,6 +3312,10 @@ msgstr "Supplying your"
msgid "Interest rate that is determined by Aave Governance. This rate may be changed over time depending on the need for the GHO supply to contract/expand. <0>Learn more</0>"
msgstr "Interest rate that is determined by Aave Governance. This rate may be changed over time depending on the need for the GHO supply to contract/expand. <0>Learn more</0>"

#: src/modules/governance/proposal/ProposalTimeline.tsx
msgid "Payload {payloadId} expired"
msgstr "Payload {payloadId} expired"

#: src/components/transactions/Emode/EmodeActions.tsx
#: src/components/transactions/Emode/EmodeModalContent.tsx
msgid "Disable E-Mode"
Expand Down Expand Up @@ -3629,6 +3637,10 @@ msgstr "Something went wrong"
msgid "Withdrawing this amount will reduce your health factor and increase risk of liquidation."
msgstr "Withdrawing this amount will reduce your health factor and increase risk of liquidation."

#: src/modules/governance/proposal/ProposalTimeline.tsx
msgid "Payload {payloadId} not executed"
msgstr "Payload {payloadId} not executed"

#: src/modules/dashboard/LiquidationRiskParametresModal/LiquidationRiskParametresModal.tsx
msgid "Your health factor and loan to value determine the assurance of your collateral. To avoid liquidations you can supply more collateral or repay borrow positions."
msgstr "Your health factor and loan to value determine the assurance of your collateral. To avoid liquidations you can supply more collateral or repay borrow positions."
Expand Down Expand Up @@ -4033,6 +4045,10 @@ msgstr "Available to borrow"
msgid "Health factor is not below the threshold"
msgstr "Health factor is not below the threshold"

#: src/modules/governance/proposal/ProposalTimeline.tsx
msgid "Payload {payloadId} cancelled"
msgstr "Payload {payloadId} cancelled"

#: src/modules/governance/proposal/ProposalTopPanel.tsx
#: src/modules/migration/MigrationTopPanel.tsx
msgid "Go Back"
Expand Down Expand Up @@ -4500,6 +4516,7 @@ msgid "In Wallet"
msgstr "In Wallet"

#: src/modules/governance/proposal/ProposalPayloads.tsx
#: src/modules/governance/proposal/ProposalTimeline.tsx
msgid "Payloads"
msgstr "Payloads"

Expand Down
2 changes: 1 addition & 1 deletion src/locales/es/messages.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/locales/fr/messages.js

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions src/modules/governance/__tests__/payloadStatus.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { getPayloadLabelKind } from '../utils/payloadStatus';

describe('getPayloadLabelKind', () => {
it('reports an executed payload as executed', () => {
expect(getPayloadLabelKind('executed')).toBe('executed');
});

// Regression guard for https://github.com/aave/interface/issues/2480: a queued payload is
// scheduled but has not run, and used to be labelled "executed" on Passed proposals.
it('reports a queued payload as not executed', () => {
expect(getPayloadLabelKind('queued')).toBe('not-executed');
});

it('does not claim execution for a payload that is only created', () => {
expect(getPayloadLabelKind('created')).toBe('pending');
});

it('reports a cancelled payload as cancelled', () => {
expect(getPayloadLabelKind('cancelled')).toBe('cancelled');
});

it('reports an expired payload as expired', () => {
expect(getPayloadLabelKind('expired')).toBe('expired');
});

// `state` is a free-form string on the cache response, so anything unrecognised must fall
// back to the non-committal label rather than asserting an on-chain action.
it.each([['partially_executed'], ['something_new'], [''], [null], [undefined]])(
'falls back to pending for %p',
(state) => {
expect(getPayloadLabelKind(state as string | null | undefined)).toBe('pending');
}
);
});
44 changes: 33 additions & 11 deletions src/modules/governance/proposal/ProposalTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import dayjs from 'dayjs';
import { ReactNode, useEffect, useMemo, useState } from 'react';
import { ArrowUpRightIcon } from 'src/components/icons/ArrowUpRightIcon';
import { useGovernanceCoreConstants } from 'src/hooks/governance/useGovernanceCoreConstants';
import { getPayloadLabelKind, PayloadLabelKind } from 'src/modules/governance/utils/payloadStatus';
import { ProposalDetail, ProposalPayload } from 'src/services/GovernanceCacheService';
import { governanceV3Config } from 'src/ui-config/governanceConfig';
import { networkConfigs } from 'src/ui-config/networksConfig';
Expand Down Expand Up @@ -90,6 +91,25 @@ const fmtRemaining = (seconds: number): string | null => {
return '<1m';
};

/**
* The sub-row label must only claim what the payload state actually supports — saying
* "executed" next to a countdown reads as a finished on-chain action (issue #2480).
*/
const payloadSubRowLabel = (kind: PayloadLabelKind, payloadId: number): ReactNode => {
switch (kind) {
case 'executed':
return <Trans>Payload {payloadId} executed</Trans>;
case 'cancelled':
return <Trans>Payload {payloadId} cancelled</Trans>;
case 'expired':
return <Trans>Payload {payloadId} expired</Trans>;
case 'not-executed':
return <Trans>Payload {payloadId} not executed</Trans>;
default:
return <Trans>Payload {payloadId} execution</Trans>;
}
};

interface SubRow {
key: string;
chainId?: number;
Expand Down Expand Up @@ -455,34 +475,36 @@ export const ProposalTimeline = ({
key: 'payloads-executed',
// "finalized" when settled with some that didn't execute (expired/cancelled), so the yellow
// "0 / N executed" isn't mistaken for still-running.
name:
settled && !allExecuted ? (
<Trans>Payloads finalized</Trans>
) : (
<Trans>Payloads executed</Trans>
),
name: allExecuted ? (
<Trans>Payloads executed</Trans>
) : settled ? (
<Trans>Payloads finalized</Trans>
) : (
<Trans>Payloads</Trans>
),
status: allExecuted ? 'ok' : settled ? 'settled' : anyQueued ? 'now' : 'pending',
value: executedValue,
valueKind: 'count',
subRows: list.map((pl) => {
const plDelay = pl.delaySeconds ?? FALLBACK_PAYLOAD_TIMELOCK_SECONDS;
const labelKind = getPayloadLabelKind(pl.state);
let value: ReactNode =
fmtEstimate(clampFuture(projExecuted != null ? projExecuted + plDelay : null)) ??
'pending';
let tone: SubRow['tone'] = 'estimate';
let txHash: string | null | undefined;
if (pl.state === 'executed') {
if (labelKind === 'executed') {
value = fmtIso(pl.executedAt) ?? 'executed';
tone = 'done';
txHash = pl.executedTxHash;
} else if (pl.state === 'cancelled') {
} else if (labelKind === 'cancelled') {
value = 'cancelled';
tone = 'wait';
txHash = pl.cancelledTxHash;
} else if (pl.state === 'expired') {
} else if (labelKind === 'expired') {
value = 'expired';
tone = 'wait';
} else if (pl.state === 'queued') {
} else if (labelKind === 'not-executed') {
// Payload execution timelock: executable at queuedAt + delay (real, from the snapshot).
const qUnix = isoToUnix(pl.queuedAt);
const execAt = qUnix != null ? qUnix + plDelay : null;
Expand All @@ -493,7 +515,7 @@ export const ProposalTimeline = ({
return {
key: `e-${pl.chainId}-${pl.payloadId}`,
chainId: pl.chainId,
label: <Trans>Payload {pl.payloadId} executed</Trans>,
label: payloadSubRowLabel(labelKind, pl.payloadId),
value,
tone,
txHash,
Expand Down
30 changes: 30 additions & 0 deletions src/modules/governance/utils/payloadStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Which claim the timeline is allowed to make about a payload, derived from the
* payload state reported by the governance cache.
*
* Kept separate from the rendering so the mapping can be unit tested: asserting
* that a non-executed payload never produces an "executed" label is the whole
* point (see https://github.com/aave/interface/issues/2480).
*/
export type PayloadLabelKind = 'executed' | 'not-executed' | 'cancelled' | 'expired' | 'pending';

/**
* `ProposalPayload.state` is a free-form string on the cache response, so an
* unrecognised value must fall back to the non-committal 'pending' — never to
* 'executed', which would assert an on-chain action that may not have happened.
*/
export const getPayloadLabelKind = (state: string | null | undefined): PayloadLabelKind => {
switch (state) {
case 'executed':
return 'executed';
case 'cancelled':
return 'cancelled';
case 'expired':
return 'expired';
// Queued means the timelock is running: bridged and scheduled, but not run yet.
case 'queued':
return 'not-executed';
default:
return 'pending';
}
};