Skip to content

Prevent proposals with duplicate actions in GovernorTimelockCompound#6469

Open
Sanenelisiwe1975 wants to merge 1 commit intoOpenZeppelin:masterfrom
Sanenelisiwe1975:fix/governor-timelock-duplicate-actions
Open

Prevent proposals with duplicate actions in GovernorTimelockCompound#6469
Sanenelisiwe1975 wants to merge 1 commit intoOpenZeppelin:masterfrom
Sanenelisiwe1975:fix/governor-timelock-duplicate-actions

Conversation

@Sanenelisiwe1975
Copy link
Copy Markdown

The Compound Timelock queues each proposal action as a separate transaction identified by keccak256(target, value, calldata, eta). Two identical actions in the same proposal produce the same hash and the second queue call reverts, leaving the proposal stuck.

Override _propose to detect duplicate (target, value, calldata) tuples and revert with GovernorTimelockCompoundDuplicateProposalAction before the proposal is stored, rather than failing silently at queue time.

Fixes #6431

Fixes #????

PR Checklist

  • Tests
  • Documentation
  • Changeset entry (run npx changeset add)

@Sanenelisiwe1975 Sanenelisiwe1975 requested a review from a team as a code owner April 7, 2026 11:08
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 7, 2026

🦋 Changeset detected

Latest commit: 97b6534

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
openzeppelin-solidity Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 7, 2026

Walkthrough

This change adds validation to the GovernorTimelockCompound contract to reject proposals containing duplicate actions at submission time. A new custom error GovernorTimelockCompoundDuplicateProposalAction is introduced, and the _propose method is overridden to scan proposal arrays for duplicate actions (matching target, value, and calldata hash). When a duplicate is detected, the proposal submission immediately reverts. The corresponding mock contract and test suite are updated to accommodate this validation behavior, moving the duplicate-action check from the queueing phase to the proposal-creation phase.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Prevent proposals with duplicate actions in GovernorTimelockCompound' clearly and specifically describes the main change in this PR.
Description check ✅ Passed The description accurately explains the issue with duplicate actions in Compound Timelock and the solution being implemented.
Linked Issues check ✅ Passed The PR fulfills the core requirement from issue #6431 by overriding _propose to detect and reject duplicate (target, value, calldata) tuples at submission time.
Out of Scope Changes check ✅ Passed All changes are focused on implementing the duplicate action detection feature; no out-of-scope modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@contracts/governance/extensions/GovernorTimelockCompound.sol`:
- Around line 75-93: In _propose, validate array lengths before performing the
duplicate-action nested loop: check that targets.length == values.length and
targets.length == calldatas.length and revert with
GovernorInvalidProposalLength() if not; this ensures the function does not read
out-of-bounds (e.g., when accessing values[i] or calldatas[i]) and preserves the
original error semantics before calling super._propose(targets, values,
calldatas, description, proposer) and keeping the duplicate-action check that
reverts with GovernorTimelockCompoundDuplicateProposalAction(i).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 205db154-88b8-400d-9798-8ac8ea3f28e9

📥 Commits

Reviewing files that changed from the base of the PR and between 9cfdccd and e677fbb.

📒 Files selected for processing (4)
  • .changeset/governor-timelock-duplicate-actions.md
  • contracts/governance/extensions/GovernorTimelockCompound.sol
  • contracts/mocks/governance/GovernorTimelockCompoundMock.sol
  • test/governance/extensions/GovernorTimelockCompound.test.js

Comment on lines +75 to +93
function _propose(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
string memory description,
address proposer
) internal virtual override returns (uint256) {
for (uint256 i = 0; i < targets.length; ++i) {
for (uint256 j = i + 1; j < targets.length; ++j) {
if (
targets[i] == targets[j] &&
values[i] == values[j] &&
keccak256(calldatas[i]) == keccak256(calldatas[j])
) {
revert GovernorTimelockCompoundDuplicateProposalAction(i);
}
}
}
return super._propose(targets, values, calldatas, description, proposer);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Changed override:"
rg -n -C6 'function _propose\(|GovernorTimelockCompoundDuplicateProposalAction|keccak256\(calldatas\[i\]\)' contracts/governance/extensions/GovernorTimelockCompound.sol

echo
echo "Base Governor length validation:"
rg -n -C8 'function _propose\(|GovernorInvalidProposalLength' contracts/governance/Governor.sol

echo
echo "Existing tests/assertions that mention GovernorInvalidProposalLength:"
rg -n -C2 'GovernorInvalidProposalLength' test

Repository: OpenZeppelin/openzeppelin-contracts

Length of output: 4770


Add array-length validation before the duplicate-action scan.

The nested loop starting at line 82 accesses values[i] and calldatas[i] before super._propose() runs its length check. If targets.length exceeds values.length or calldatas.length, the override now panics on an out-of-bounds read instead of reverting with the expected GovernorInvalidProposalLength error, breaking the contract's error semantics and failing existing tests.

Minimal fix
 function _propose(
     address[] memory targets,
     uint256[] memory values,
     bytes[] memory calldatas,
     string memory description,
     address proposer
 ) internal virtual override returns (uint256) {
+    if (targets.length != values.length || targets.length != calldatas.length) {
+        return super._propose(targets, values, calldatas, description, proposer);
+    }
+
     for (uint256 i = 0; i < targets.length; ++i) {
         for (uint256 j = i + 1; j < targets.length; ++j) {
             if (
                 targets[i] == targets[j] &&
                 values[i] == values[j] &&
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function _propose(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
string memory description,
address proposer
) internal virtual override returns (uint256) {
for (uint256 i = 0; i < targets.length; ++i) {
for (uint256 j = i + 1; j < targets.length; ++j) {
if (
targets[i] == targets[j] &&
values[i] == values[j] &&
keccak256(calldatas[i]) == keccak256(calldatas[j])
) {
revert GovernorTimelockCompoundDuplicateProposalAction(i);
}
}
}
return super._propose(targets, values, calldatas, description, proposer);
function _propose(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
string memory description,
address proposer
) internal virtual override returns (uint256) {
if (targets.length != values.length || targets.length != calldatas.length) {
return super._propose(targets, values, calldatas, description, proposer);
}
for (uint256 i = 0; i < targets.length; ++i) {
for (uint256 j = i + 1; j < targets.length; ++j) {
if (
targets[i] == targets[j] &&
values[i] == values[j] &&
keccak256(calldatas[i]) == keccak256(calldatas[j])
) {
revert GovernorTimelockCompoundDuplicateProposalAction(i);
}
}
}
return super._propose(targets, values, calldatas, description, proposer);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/governance/extensions/GovernorTimelockCompound.sol` around lines 75
- 93, In _propose, validate array lengths before performing the duplicate-action
nested loop: check that targets.length == values.length and targets.length ==
calldatas.length and revert with GovernorInvalidProposalLength() if not; this
ensures the function does not read out-of-bounds (e.g., when accessing values[i]
or calldatas[i]) and preserves the original error semantics before calling
super._propose(targets, values, calldatas, description, proposer) and keeping
the duplicate-action check that reverts with
GovernorTimelockCompoundDuplicateProposalAction(i).

@Sanenelisiwe1975 Sanenelisiwe1975 force-pushed the fix/governor-timelock-duplicate-actions branch from e677fbb to c0e275c Compare April 7, 2026 11:30
The Compound Timelock queues each proposal action as a separate
transaction identified by keccak256(target, value, calldata, eta).
Two identical actions in the same proposal produce the same hash and
the second queue call reverts, leaving the proposal stuck.

Override _propose to detect duplicate (target, value, calldata) tuples
and revert with GovernorTimelockCompoundDuplicateProposalAction before
the proposal is stored, rather than failing silently at queue time.

If the arrays are mismatched in length, the duplicate check is skipped
and super._propose handles the validation with GovernorInvalidProposalLength.

Fixes OpenZeppelin#6431
@Sanenelisiwe1975 Sanenelisiwe1975 force-pushed the fix/governor-timelock-duplicate-actions branch from c0e275c to 97b6534 Compare April 7, 2026 11:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Prevent proposals with duplicate actions from being submitted in GovernorTimelockCompound

2 participants