Skip to content

fix: reject proposals with duplicate actions in GovernorTimelockCompound#6547

Open
aglichandrap wants to merge 1 commit into
OpenZeppelin:masterfrom
aglichandrap:fix/governor-duplicate-actions
Open

fix: reject proposals with duplicate actions in GovernorTimelockCompound#6547
aglichandrap wants to merge 1 commit into
OpenZeppelin:masterfrom
aglichandrap:fix/governor-duplicate-actions

Conversation

@aglichandrap

Copy link
Copy Markdown

Problem

Proposals containing 2+ actions with identical (target, value, calldata) will fail during queueing in _queueOperations. The Compound timelock generates transaction hashes from keccak256(abi.encode(targets[i], values[i], "", calldatas[i], etaSeconds)), so duplicate actions produce identical hashes. The first action queues successfully, but the second triggers GovernorAlreadyQueuedProposal because the hash is already marked as queued.

This can be circumvented by appending extra bytes to calldata, but users may not realize their proposal is unexecutable until after voting ends.

Solution

Override _propose in GovernorTimelockCompound to detect duplicate actions at proposal creation time, providing a clear error before any gas is wasted on voting.

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 GovernorDuplicateAction(targets[i], values[i], calldatas[i]);
            }
        }
    }
    return super._propose(targets, values, calldatas, description, proposer);
}

Complexity

O(n²) pairwise comparison where n = number of actions. For typical proposals (n < 20), this is negligible gas cost.

Tests

TODO: Add test cases for:

  • Proposal with no duplicates → succeeds
  • Proposal with duplicate actions → reverts with GovernorDuplicateAction
  • Proposal with same target/value but different calldata → succeeds

Fixes #6431

Proposals containing duplicate (target, value, calldata) actions would
fail silently during queueing because the Compound timelock generates
identical transaction hashes. Override _propose to detect duplicates
early and revert with a clear GovernorDuplicateAction error.

Fixes OpenZeppelin#6431
@aglichandrap aglichandrap requested a review from a team as a code owner May 24, 2026 14:33
@changeset-bot

changeset-bot Bot commented May 24, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: baf1ed9

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds duplicate action validation to GovernorTimelockCompound at proposal creation time. A new custom error GovernorDuplicateAction is defined to report violations. The _propose function is overridden to scan all action pairs in the proposal, comparing target address, value, and calldata hash to detect duplicates. If any duplicate is found, the proposal is rejected immediately; otherwise, control passes to the parent implementation. This prevents creation of proposals that would later fail during queueing in Compound Governance due to duplicate actions.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: reject proposals with duplicate actions in GovernorTimelockCompound' accurately summarizes the main change—preventing duplicate actions in proposals at creation time.
Description check ✅ Passed The description comprehensively explains the problem (duplicate actions failing at queueing), the solution (rejecting duplicates at proposal creation), and includes the implementation logic with complexity analysis.
Linked Issues check ✅ Passed The pull request fully addresses issue #6431's objectives: the _propose override detects duplicate actions (same target, value, and calldata) at creation time and rejects them with GovernorDuplicateAction error.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the PR objectives: adding the GovernorDuplicateAction error and _propose override to detect and reject duplicate actions in GovernorTimelockCompound.
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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contracts/governance/extensions/GovernorTimelockCompound.sol`:
- Line 66: The custom error declaration GovernorDuplicateAction currently uses
an unsupported data-location specifier (`bytes calldata`); update the error
signature to use `bytes` instead (remove `calldata`) so the compiler accepts it,
e.g., change the error declaration for GovernorDuplicateAction(address target,
uint256 value, bytes calldata) to GovernorDuplicateAction(address target,
uint256 value, bytes) and leave all revert calls that pass calldatas[i]
unchanged.
🪄 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: 9e059572-bcf1-4761-a148-83c0dd13697f

📥 Commits

Reviewing files that changed from the base of the PR and between 5a3b28f and baf1ed9.

📒 Files selected for processing (1)
  • contracts/governance/extensions/GovernorTimelockCompound.sol

/**
* @dev Emitted when a proposal contains duplicate actions.
*/
error GovernorDuplicateAction(address target, uint256 value, bytes calldata);

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Attempt to check Solidity syntax for custom error definitions with data location specifiers

# Check if similar error definitions exist in the codebase that might indicate the pattern used
rg -n "error.*\(.*bytes (memory|calldata|storage)" --type sol

# Search for other custom error definitions to verify the standard pattern
rg -n "^\s*error\s+\w+\(" --type sol -A 0 | head -20

Repository: OpenZeppelin/openzeppelin-contracts

Length of output: 135


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Locate and print the surrounding lines for the reported error
FILE="contracts/governance/extensions/GovernorTimelockCompound.sol"
if [ -f "$FILE" ]; then
  echo "== Found $FILE =="
  nl -ba "$FILE" | sed -n '50,85p'
else
  echo "== File not found: $FILE =="
  # Try to locate similar file names
  fd -a "GovernorTimelockCompound.sol" . || true
fi

# 2) Search for custom errors that use bytes with data locations
# (data location specifiers in error parameters are the concern: memory/calldata/storage)
echo
echo "== Search: errors with 'bytes calldata' =="
rg -n "error\s+\w+\s*\([^)]*bytes\s+(calldata|memory|storage)\b" -g '*.sol' || true

echo
echo "== Search: errors with 'bytes' and any data location words =="
rg -n "error\s+\w+\s*\([^)]*bytes\s+(calldata|memory|storage)\b" -g '*.sol' || true

# 3) Search for any 'error <Name>(' occurrences (quick context)
echo
echo "== Search: custom error declarations (first 50 matches) =="
rg -n "^\s*error\s+\w+\s*\(" -g '*.sol' | head -50 || true

Repository: OpenZeppelin/openzeppelin-contracts

Length of output: 213


🌐 Web query:

Can Solidity custom errors include data location specifiers like bytes calldataorbytes memory in the error parameter list?

💡 Result:

No. Solidity custom error parameter lists do not allow data location specifiers like bytes calldata or bytes memory. Reason (from Solidity language definition): the grammar for an error definition uses error-parameter, which (per the Solidity grammar) is just a type parameter entry and does not provide a syntax production that includes a data-location annotation in the error parameter list; data-location exists as a separate grammar element but is used for other declarations (e.g., function parameters/return types) rather than custom error parameters [1]. Additionally, Solidity’s type system defines data locations as “memory”, “storage”, and “calldata” for reference types, but it describes these in the context of where data is stored for variables/parameters; it does not indicate that error parameter declarations support specifying such locations inline [2]. So you must write custom error parameters without calldata/memory annotations, e.g.: - error Invalid(bytes value); (no bytes calldata / bytes memory) If you need calldata vs memory optimization, that’s about function parameter locations, not custom error type declarations.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="contracts/governance/extensions/GovernorTimelockCompound.sol"

echo "== Lines 55-75 from $FILE =="
awk 'NR>=55 && NR<=75 {printf "%d:%s\n", NR, $0}' "$FILE"

echo
echo "== Search: errors that include bytes with a data location keyword =="
rg -n "error\s+\w+\s*\([^)]*bytes\s+(calldata|memory|storage)\b" -g '*.sol' .

echo
echo "== Search: custom error declarations (first 30 matches) =="
rg -n "^\s*error\s+\w+\s*\(" -g '*.sol' . | head -30

Repository: OpenZeppelin/openzeppelin-contracts

Length of output: 4324


Fix custom error parameter type to use bytes (remove calldata)

GovernorDuplicateAction declares bytes calldata in the custom error parameter list, but Solidity custom error parameters don’t support data-location specifiers; this should be bytes so the file compiles. The revert already passes calldatas[i] as the bytes payload, so no location is needed.

🔧 Proposed fix
-    error GovernorDuplicateAction(address target, uint256 value, bytes calldata);
+    error GovernorDuplicateAction(address target, uint256 value, bytes data);
📝 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
error GovernorDuplicateAction(address target, uint256 value, bytes calldata);
error GovernorDuplicateAction(address target, uint256 value, bytes data);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/governance/extensions/GovernorTimelockCompound.sol` at line 66, The
custom error declaration GovernorDuplicateAction currently uses an unsupported
data-location specifier (`bytes calldata`); update the error signature to use
`bytes` instead (remove `calldata`) so the compiler accepts it, e.g., change the
error declaration for GovernorDuplicateAction(address target, uint256 value,
bytes calldata) to GovernorDuplicateAction(address target, uint256 value, bytes)
and leave all revert calls that pass calldatas[i] unchanged.

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