Skip to content

Prevent duplicate actions in GovernorTimelockCompound proposals#6489

Open
JunaidCD wants to merge 3 commits intoOpenZeppelin:masterfrom
JunaidCD:prevent-duplicate-actions
Open

Prevent duplicate actions in GovernorTimelockCompound proposals#6489
JunaidCD wants to merge 3 commits intoOpenZeppelin:masterfrom
JunaidCD:prevent-duplicate-actions

Conversation

@JunaidCD
Copy link
Copy Markdown

@JunaidCD JunaidCD commented May 1, 2026

Fixes #6431

Summary

Prevent proposals with duplicate actions from being created in GovernorTimelockCompound.

Problem

Proposals containing duplicate actions (same target, value, and calldata) are accepted during propose(), but fail during queue() due to identical transaction hashes.

Solution

Add validation in _propose() to detect duplicates early and revert.

Tests

  • Added duplicate action tests
  • Updated existing tests
  • All tests passing

PR Checklist

  • Tests
  • Documentation
  • Changeset entry

@JunaidCD JunaidCD requested a review from a team as a code owner May 1, 2026 08:45
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 1, 2026

🦋 Changeset detected

Latest commit: 56a83e1

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 May 1, 2026

Walkthrough

This pull request adds duplicate-action detection to the governance proposal creation workflow in GovernorTimelockCompound. The implementation introduces validation in the _propose function to check for duplicate actions within a proposal, where duplication is determined by matching target address, ETH value amount, and calldata hash. When duplicates are detected, the proposal creation reverts with a custom error containing the indices of the duplicate actions. The changes include modifications to the main contract, a corresponding mock contract override, and comprehensive test coverage validating both acceptance of proposals with distinct actions and rejection of proposals with duplicate actions at various positions.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Prevent duplicate actions in GovernorTimelockCompound proposals' is concise, clear, and directly summarizes the main change in the changeset.
Description check ✅ Passed The description clearly relates to the changeset, explaining the problem, solution, and testing approach for preventing duplicate actions in GovernorTimelockCompound proposals.
Linked Issues check ✅ Passed The PR fully addresses issue #6431 by implementing duplicate action detection in _propose() that rejects proposals with identical target, value, and calldata combinations.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing duplicate action detection: modifications to GovernorTimelockCompound, its mock, and corresponding tests with no unrelated alterations.
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
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

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 69-89: The duplicate-action nested loop in _propose reads
values[i] and calldatas[i] without first validating array lengths, risking
out-of-bounds panics; add an explicit guard that checks targets.length ==
values.length && targets.length == calldatas.length (and revert with
GovernorInvalidProposalLength) before running the duplicate scan loop in
function _propose so the child check fails fast and the call can safely
reference values[i] and calldatas[i], then call super._propose(...) as before.
🪄 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: bc664c8b-8a0d-45bc-92c6-029798bc8543

📥 Commits

Reviewing files that changed from the base of the PR and between 9767839 and d0e2852.

📒 Files selected for processing (3)
  • contracts/governance/extensions/GovernorTimelockCompound.sol
  • contracts/mocks/governance/GovernorTimelockCompoundMock.sol
  • test/governance/extensions/GovernorTimelockCompound.test.js

Comment on lines +69 to +89
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 = 0; j < i; ++j) {
if (
targets[i] == targets[j] &&
values[i] == values[j] &&
keccak256(calldatas[i]) == keccak256(calldatas[j])
) {
revert GovernorDuplicateProposalAction(i, j);
}
}
}

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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find contracts -name "GovernorTimelockCompound.sol" -type f

Repository: OpenZeppelin/openzeppelin-contracts

Length of output: 140


🏁 Script executed:

# Also search for the error type mentioned
rg "GovernorInvalidProposalLength" --type sol

Repository: OpenZeppelin/openzeppelin-contracts

Length of output: 107


🏁 Script executed:

# Search for the parent class to understand its _propose implementation
rg "class Governor" --type sol -A 5

Repository: OpenZeppelin/openzeppelin-contracts

Length of output: 107


🏁 Script executed:

cat -n contracts/governance/extensions/GovernorTimelockCompound.sol | head -100

Repository: OpenZeppelin/openzeppelin-contracts

Length of output: 4516


🏁 Script executed:

rg "GovernorInvalidProposalLength" --max-count 10

Repository: OpenZeppelin/openzeppelin-contracts

Length of output: 820


🏁 Script executed:

rg "function _propose" --max-count 5 -B 2 -A 15

Repository: OpenZeppelin/openzeppelin-contracts

Length of output: 10455


🏁 Script executed:

cat -n contracts/governance/Governor.sol | grep -A 20 "function _propose"

Repository: OpenZeppelin/openzeppelin-contracts

Length of output: 1134


Add length guard before duplicate scan

The duplicate action check at lines 76-86 iterates targets.length and immediately dereferences values[i] and calldatas[i] without validating array lengths first. If callers pass mismatched array lengths (e.g., targets.length=2 but values.length=1), an out-of-bounds access triggers a Solidity panic rather than reverting gracefully with GovernorInvalidProposalLength. The parent Governor._propose() validates these lengths, but only after the child override executes, so it cannot prevent the panic.

Add the length validation check before the duplicate scan loop:

Suggested patch
 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 || targets.length == 0) {
+        revert GovernorInvalidProposalLength(targets.length, calldatas.length, values.length);
+    }
+
     for (uint256 i = 0; i < targets.length; ++i) {
         for (uint256 j = 0; j < i; ++j) {
             if (
                 targets[i] == targets[j] &&
                 values[i] == values[j] &&
                 keccak256(calldatas[i]) == keccak256(calldatas[j])
             ) {
                 revert GovernorDuplicateProposalAction(i, j);
             }
         }
     }

     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 69
- 89, The duplicate-action nested loop in _propose reads values[i] and
calldatas[i] without first validating array lengths, risking out-of-bounds
panics; add an explicit guard that checks targets.length == values.length &&
targets.length == calldatas.length (and revert with
GovernorInvalidProposalLength) before running the duplicate scan loop in
function _propose so the child check fails fast and the call can safely
reference values[i] and calldatas[i], then call super._propose(...) as before.

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

1 participant