Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: medium
Valid

Hash Collision Risk in Governance Timelock Operations Due to Using Proposal Description as Salt

Summary

The governance system uses proposal.descriptionHash as a salt when generating timelock operation IDs. Two different proposals with the same description and parameters will generate identical operation IDs, preventing the second proposal from being queued in the timelock.

Vulnerability Details

When a proposal is queued in the timelock, it generates an operation ID using:

[](https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/governance/proposals/Governance.sol#L490C1-L497C11)

@> bytes32 salt = proposal.descriptionHash;
bytes32 id = _timelock.hashOperationBatch(
proposal.targets,
proposal.values,
proposal.calldatas,
bytes32(0),
salt
);

If two proposals share the same description and parameters, they will generate identical operation IDs. When the second proposal tries to queue, it fails with OperationAlreadyScheduled because TimelockController::scheduleBatch checks:

[](https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/governance/proposals/TimelockController.sol#L140)

if (_operations[id].timestamp != 0) revert OperationAlreadyScheduled(id);

The issue is that using just the description hash as salt doesn't provide sufficient uniqueness for operation IDs.

Proof of Code:

Add this test to Governance.test.js

describe("Governance Hash Collision", () => {
it.only("should handle hash collisions correctly", async () => {
// Initial setup - mint tokens to user1
await veToken.mock_setInitialVotingPower(
await user1.getAddress(),
ethers.parseEther("100000")
);
// Create first proposal
const targets = [await testTarget.getAddress()];
const values = [0];
const calldatas = [testTarget.interface.encodeFunctionData("setValue", [0])];
const description = "Test Proposal";
// Create first proposal
await governance.connect(user1).propose(
targets,
values,
calldatas,
description,
0 // ParameterChange
);
// Move time forward past voting delay
await time.increase(await governance.votingDelay() + 1n);
// Cast vote
await governance.connect(user1).castVote(0, true);
// Move time forward past voting period
await time.increase(await governance.votingPeriod() + 1n);
// Execute first proposal
await governance.connect(owner).execute(0);
// Move time forward slightly
await time.increase(1);
// Setup for second proposal
await veToken.mock_setInitialVotingPower(
await user1.getAddress(),
ethers.parseEther("100000")
);
// Create second proposal with same parameters
await governance.connect(user1).propose(
targets,
values,
calldatas,
description, // Same description = same salt
0 // ParameterChange
);
// Move time forward and vote
await time.increase(await governance.votingDelay() + 1n);
await governance.connect(user1).castVote(1, true);
// Move time forward past voting period
await time.increase(await governance.votingPeriod() + 1n);
await governance.connect(owner).execute(0);
// Try to execute same id proposal - should revert due to hash collision
await expect(
governance.connect(owner).execute(1)
).to.be.revertedWithCustomError(
timelock,
"OperationAlreadyScheduled"
);
});

Impact

  • Prevents legitimate proposals with matching descriptions from being queued

  • Reduces governance system reliability by allowing unintended proposal blocking

Tools Used

Foundry

Recommendations

Use unique salts for each proposal by incorporating the proposal ID:

bytes32 salt = keccak256(abi.encode(proposalId, block.timestamp));

This ensures each proposal gets a unique operation ID in the timelock, even if other parameters match.

Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Governance generates non-unique timelock operation IDs for different proposals with identical parameters, allowing timelock bypass and proposal DoS attacks

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Governance generates non-unique timelock operation IDs for different proposals with identical parameters, allowing timelock bypass and proposal DoS attacks

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.