Skip to content

Commit 2957bbd

Browse files
committed
fix(governance): only label payloads executed once they have run
The proposal timeline rendered "Payload N executed" for every payload, including ones still queued or created, so Passed proposals appeared to have executed. Derive the label from the payload state instead. Closes #2480
1 parent 32cc548 commit 2957bbd

8 files changed

Lines changed: 122 additions & 19 deletions

File tree

‎src/locales/el/messages.js‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/locales/en/messages.js‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/locales/en/messages.po‎

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,10 @@ msgstr "Aave is a fully decentralized, community governed protocol by the AAVE t
271271
msgid "Please enter a valid wallet address."
272272
msgstr "Please enter a valid wallet address."
273273

274+
#: src/modules/governance/proposal/ProposalTimeline.tsx
275+
msgid "Payload {payloadId} executed"
276+
msgstr "Payload {payloadId} executed"
277+
274278
#: src/components/ConnectWalletPaper.tsx
275279
msgid "Connect your wallet to see your supplies, borrowings, and open positions."
276280
msgstr "Connect your wallet to see your supplies, borrowings, and open positions."
@@ -1812,10 +1816,6 @@ msgstr "Email"
18121816
msgid "here"
18131817
msgstr "here"
18141818

1815-
#: src/modules/governance/proposal/ProposalTimeline.tsx
1816-
msgid "Payload {0} executed"
1817-
msgstr "Payload {0} executed"
1818-
18191819
#: src/modules/staking/StakingPanel.tsx
18201820
#: src/modules/umbrella/UnstakeModal.tsx
18211821
msgid "Unstake"
@@ -1974,6 +1974,10 @@ msgstr "The Aave Balancer Pool Token (ABPT) is a liquidity pool token. You can r
19741974
msgid "Merit Rewards"
19751975
msgstr "Merit Rewards"
19761976

1977+
#: src/modules/governance/proposal/ProposalTimeline.tsx
1978+
msgid "Payload {payloadId} execution"
1979+
msgstr "Payload {payloadId} execution"
1980+
19771981
#: src/components/transactions/SGhoVault/SGhoVaultWithdrawActions.tsx
19781982
#: src/components/transactions/Withdraw/WithdrawAndUnwrapActions.tsx
19791983
msgid "Withdrawing"
@@ -3308,6 +3312,10 @@ msgstr "Supplying your"
33083312
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>"
33093313
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>"
33103314

3315+
#: src/modules/governance/proposal/ProposalTimeline.tsx
3316+
msgid "Payload {payloadId} expired"
3317+
msgstr "Payload {payloadId} expired"
3318+
33113319
#: src/components/transactions/Emode/EmodeActions.tsx
33123320
#: src/components/transactions/Emode/EmodeModalContent.tsx
33133321
msgid "Disable E-Mode"
@@ -3629,6 +3637,10 @@ msgstr "Something went wrong"
36293637
msgid "Withdrawing this amount will reduce your health factor and increase risk of liquidation."
36303638
msgstr "Withdrawing this amount will reduce your health factor and increase risk of liquidation."
36313639

3640+
#: src/modules/governance/proposal/ProposalTimeline.tsx
3641+
msgid "Payload {payloadId} not executed"
3642+
msgstr "Payload {payloadId} not executed"
3643+
36323644
#: src/modules/dashboard/LiquidationRiskParametresModal/LiquidationRiskParametresModal.tsx
36333645
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."
36343646
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."
@@ -4033,6 +4045,10 @@ msgstr "Available to borrow"
40334045
msgid "Health factor is not below the threshold"
40344046
msgstr "Health factor is not below the threshold"
40354047

4048+
#: src/modules/governance/proposal/ProposalTimeline.tsx
4049+
msgid "Payload {payloadId} cancelled"
4050+
msgstr "Payload {payloadId} cancelled"
4051+
40364052
#: src/modules/governance/proposal/ProposalTopPanel.tsx
40374053
#: src/modules/migration/MigrationTopPanel.tsx
40384054
msgid "Go Back"
@@ -4500,6 +4516,7 @@ msgid "In Wallet"
45004516
msgstr "In Wallet"
45014517

45024518
#: src/modules/governance/proposal/ProposalPayloads.tsx
4519+
#: src/modules/governance/proposal/ProposalTimeline.tsx
45034520
msgid "Payloads"
45044521
msgstr "Payloads"
45054522

‎src/locales/es/messages.js‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/locales/fr/messages.js‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { getPayloadLabelKind } from '../utils/payloadStatus';
2+
3+
describe('getPayloadLabelKind', () => {
4+
it('reports an executed payload as executed', () => {
5+
expect(getPayloadLabelKind('executed')).toBe('executed');
6+
});
7+
8+
// Regression guard for https://github.com/aave/interface/issues/2480: a queued payload is
9+
// scheduled but has not run, and used to be labelled "executed" on Passed proposals.
10+
it('reports a queued payload as not executed', () => {
11+
expect(getPayloadLabelKind('queued')).toBe('not-executed');
12+
});
13+
14+
it('does not claim execution for a payload that is only created', () => {
15+
expect(getPayloadLabelKind('created')).toBe('pending');
16+
});
17+
18+
it('reports a cancelled payload as cancelled', () => {
19+
expect(getPayloadLabelKind('cancelled')).toBe('cancelled');
20+
});
21+
22+
it('reports an expired payload as expired', () => {
23+
expect(getPayloadLabelKind('expired')).toBe('expired');
24+
});
25+
26+
// `state` is a free-form string on the cache response, so anything unrecognised must fall
27+
// back to the non-committal label rather than asserting an on-chain action.
28+
it.each([['partially_executed'], ['something_new'], [''], [null], [undefined]])(
29+
'falls back to pending for %p',
30+
(state) => {
31+
expect(getPayloadLabelKind(state as string | null | undefined)).toBe('pending');
32+
}
33+
);
34+
});

‎src/modules/governance/proposal/ProposalTimeline.tsx‎

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import dayjs from 'dayjs';
1616
import { ReactNode, useEffect, useMemo, useState } from 'react';
1717
import { ArrowUpRightIcon } from 'src/components/icons/ArrowUpRightIcon';
1818
import { useGovernanceCoreConstants } from 'src/hooks/governance/useGovernanceCoreConstants';
19+
import { getPayloadLabelKind, PayloadLabelKind } from 'src/modules/governance/utils/payloadStatus';
1920
import { ProposalDetail, ProposalPayload } from 'src/services/GovernanceCacheService';
2021
import { governanceV3Config } from 'src/ui-config/governanceConfig';
2122
import { networkConfigs } from 'src/ui-config/networksConfig';
@@ -90,6 +91,25 @@ const fmtRemaining = (seconds: number): string | null => {
9091
return '<1m';
9192
};
9293

94+
/**
95+
* The sub-row label must only claim what the payload state actually supports — saying
96+
* "executed" next to a countdown reads as a finished on-chain action (issue #2480).
97+
*/
98+
const payloadSubRowLabel = (kind: PayloadLabelKind, payloadId: number): ReactNode => {
99+
switch (kind) {
100+
case 'executed':
101+
return <Trans>Payload {payloadId} executed</Trans>;
102+
case 'cancelled':
103+
return <Trans>Payload {payloadId} cancelled</Trans>;
104+
case 'expired':
105+
return <Trans>Payload {payloadId} expired</Trans>;
106+
case 'not-executed':
107+
return <Trans>Payload {payloadId} not executed</Trans>;
108+
default:
109+
return <Trans>Payload {payloadId} execution</Trans>;
110+
}
111+
};
112+
93113
interface SubRow {
94114
key: string;
95115
chainId?: number;
@@ -455,34 +475,36 @@ export const ProposalTimeline = ({
455475
key: 'payloads-executed',
456476
// "finalized" when settled with some that didn't execute (expired/cancelled), so the yellow
457477
// "0 / N executed" isn't mistaken for still-running.
458-
name:
459-
settled && !allExecuted ? (
460-
<Trans>Payloads finalized</Trans>
461-
) : (
462-
<Trans>Payloads executed</Trans>
463-
),
478+
name: allExecuted ? (
479+
<Trans>Payloads executed</Trans>
480+
) : settled ? (
481+
<Trans>Payloads finalized</Trans>
482+
) : (
483+
<Trans>Payloads</Trans>
484+
),
464485
status: allExecuted ? 'ok' : settled ? 'settled' : anyQueued ? 'now' : 'pending',
465486
value: executedValue,
466487
valueKind: 'count',
467488
subRows: list.map((pl) => {
468489
const plDelay = pl.delaySeconds ?? FALLBACK_PAYLOAD_TIMELOCK_SECONDS;
490+
const labelKind = getPayloadLabelKind(pl.state);
469491
let value: ReactNode =
470492
fmtEstimate(clampFuture(projExecuted != null ? projExecuted + plDelay : null)) ??
471493
'pending';
472494
let tone: SubRow['tone'] = 'estimate';
473495
let txHash: string | null | undefined;
474-
if (pl.state === 'executed') {
496+
if (labelKind === 'executed') {
475497
value = fmtIso(pl.executedAt) ?? 'executed';
476498
tone = 'done';
477499
txHash = pl.executedTxHash;
478-
} else if (pl.state === 'cancelled') {
500+
} else if (labelKind === 'cancelled') {
479501
value = 'cancelled';
480502
tone = 'wait';
481503
txHash = pl.cancelledTxHash;
482-
} else if (pl.state === 'expired') {
504+
} else if (labelKind === 'expired') {
483505
value = 'expired';
484506
tone = 'wait';
485-
} else if (pl.state === 'queued') {
507+
} else if (labelKind === 'not-executed') {
486508
// Payload execution timelock: executable at queuedAt + delay (real, from the snapshot).
487509
const qUnix = isoToUnix(pl.queuedAt);
488510
const execAt = qUnix != null ? qUnix + plDelay : null;
@@ -493,7 +515,7 @@ export const ProposalTimeline = ({
493515
return {
494516
key: `e-${pl.chainId}-${pl.payloadId}`,
495517
chainId: pl.chainId,
496-
label: <Trans>Payload {pl.payloadId} executed</Trans>,
518+
label: payloadSubRowLabel(labelKind, pl.payloadId),
497519
value,
498520
tone,
499521
txHash,
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Which claim the timeline is allowed to make about a payload, derived from the
3+
* payload state reported by the governance cache.
4+
*
5+
* Kept separate from the rendering so the mapping can be unit tested: asserting
6+
* that a non-executed payload never produces an "executed" label is the whole
7+
* point (see https://github.com/aave/interface/issues/2480).
8+
*/
9+
export type PayloadLabelKind = 'executed' | 'not-executed' | 'cancelled' | 'expired' | 'pending';
10+
11+
/**
12+
* `ProposalPayload.state` is a free-form string on the cache response, so an
13+
* unrecognised value must fall back to the non-committal 'pending' — never to
14+
* 'executed', which would assert an on-chain action that may not have happened.
15+
*/
16+
export const getPayloadLabelKind = (state: string | null | undefined): PayloadLabelKind => {
17+
switch (state) {
18+
case 'executed':
19+
return 'executed';
20+
case 'cancelled':
21+
return 'cancelled';
22+
case 'expired':
23+
return 'expired';
24+
// Queued means the timelock is running: bridged and scheduled, but not run yet.
25+
case 'queued':
26+
return 'not-executed';
27+
default:
28+
return 'pending';
29+
}
30+
};

0 commit comments

Comments
 (0)