Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/silver-falcons-lay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`Governor`: Strict enforcement of the expected proposal state depending on `proposalNeedsQueuing` when calling `execute`.
3 changes: 2 additions & 1 deletion contracts/governance/Governor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -394,10 +394,11 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
bytes32 descriptionHash
) public payable virtual returns (uint256) {
uint256 proposalId = getProposalId(targets, values, calldatas, descriptionHash);
bool needsQueueing = proposalNeedsQueuing(proposalId);

_validateStateBitmap(
proposalId,
_encodeStateBitmap(ProposalState.Succeeded) | _encodeStateBitmap(ProposalState.Queued)
_encodeStateBitmap(needsQueueing ? ProposalState.Queued : ProposalState.Succeeded)
);

// mark as executed before calls to avoid reentrancy
Expand Down
5 changes: 0 additions & 5 deletions contracts/governance/IGovernor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,6 @@ interface IGovernor is IERC165, IERC6372 {
*/
error GovernorQueueNotImplemented();

/**
* @dev The proposal hasn't been queued yet.
*/
error GovernorNotQueuedProposal(uint256 proposalId);

/**
* @dev The proposal has already been queued.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,6 @@ abstract contract GovernorTimelockCompound is Governor {
bytes32 /*descriptionHash*/
) internal virtual override {
uint256 etaSeconds = proposalEta(proposalId);
if (etaSeconds == 0) {
revert GovernorNotQueuedProposal(proposalId);
}
Comment on lines -97 to -99
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is no longer necessary because Governor.execute enforces status = Queued

Address.sendValue(payable(_timelock), msg.value);
for (uint256 i = 0; i < targets.length; ++i) {
_timelock.executeTransaction(targets[i], values[i], "", calldatas[i], etaSeconds);
Expand Down
12 changes: 6 additions & 6 deletions test/governance/Governor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ describe('Governor', function () {
.withArgs(
this.proposal.id,
ProposalState.Active,
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded]),
);
});

Expand All @@ -417,7 +417,7 @@ describe('Governor', function () {
.withArgs(
this.proposal.id,
ProposalState.Active,
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded]),
);
});

Expand All @@ -430,7 +430,7 @@ describe('Governor', function () {
.withArgs(
this.proposal.id,
ProposalState.Active,
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded]),
);
});

Expand Down Expand Up @@ -481,7 +481,7 @@ describe('Governor', function () {
.withArgs(
this.proposal.id,
ProposalState.Executed,
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded]),
);
});
});
Expand Down Expand Up @@ -569,7 +569,7 @@ describe('Governor', function () {
.withArgs(
this.proposal.id,
ProposalState.Canceled,
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded]),
);
});

Expand All @@ -587,7 +587,7 @@ describe('Governor', function () {
.withArgs(
this.proposal.id,
ProposalState.Canceled,
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded]),
);
});

Expand Down
58 changes: 50 additions & 8 deletions test/governance/extensions/GovernorTimelockAccess.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,42 @@ describe('GovernorTimelockAccess', function () {
// Set base delay
await this.mock.$_setBaseDelaySeconds(baseDelay);

await this.helper.setProposal([this.restricted.operation], 'descr');
const { id } = await this.helper.setProposal([this.restricted.operation], 'descr');
await this.helper.propose();
expect(await this.mock.proposalNeedsQueuing(this.helper.currentProposal.id)).to.be.false;
expect(await this.mock.proposalNeedsQueuing(id)).to.be.false;

await this.helper.waitForSnapshot();
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
await this.helper.waitForDeadline();

// No need for queuing, so it should not revert
await expect(this.helper.execute()).to.not.be.reverted;
});

it('does need to queue proposals with base delay', async function () {
const roleId = 1n;
const executionDelay = 0n;
const baseDelay = 10n;

// Set execution delay
await this.manager.connect(this.admin).setTargetFunctionRole(this.receiver, [this.restricted.selector], roleId);
await this.manager.connect(this.admin).grantRole(roleId, this.mock, executionDelay);

// Set base delay
await this.mock.$_setBaseDelaySeconds(baseDelay);

const { id } = await this.helper.setProposal([this.restricted.operation], 'descr');
await this.helper.propose();
expect(await this.mock.proposalNeedsQueuing(id)).to.be.true;

await this.helper.waitForSnapshot();
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
await this.helper.waitForDeadline();

// Not queued, so it should revert
await expect(this.helper.execute())
.to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
.withArgs(id, ProposalState.Succeeded, GovernorHelper.proposalStatesToBitMap([ProposalState.Queued]));
});

it('needs to queue proposals with any delay', async function () {
Expand All @@ -257,12 +290,21 @@ describe('GovernorTimelockAccess', function () {
// Set base delay
await this.mock.$_setBaseDelaySeconds(baseDelay);

await this.helper.setProposal(
const { id } = await this.helper.setProposal(
[this.restricted.operation],
`executionDelay=${executionDelay.toString()}}baseDelay=${baseDelay.toString()}}`,
);
await this.helper.propose();
expect(await this.mock.proposalNeedsQueuing(this.helper.currentProposal.id)).to.be.true;
expect(await this.mock.proposalNeedsQueuing(id)).to.be.true;

await this.helper.waitForSnapshot();
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
await this.helper.waitForDeadline();

// Not queued, so it should revert
await expect(this.helper.execute())
.to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
.withArgs(id, ProposalState.Succeeded, GovernorHelper.proposalStatesToBitMap([ProposalState.Queued]));
}
});

Expand Down Expand Up @@ -556,7 +598,7 @@ describe('GovernorTimelockAccess', function () {
.withArgs(
this.proposal.id,
ProposalState.Canceled,
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
GovernorHelper.proposalStatesToBitMap([ProposalState.Queued]), // proposal needs queueing
);
});

Expand Down Expand Up @@ -602,7 +644,7 @@ describe('GovernorTimelockAccess', function () {
.withArgs(
original.currentProposal.id,
ProposalState.Canceled,
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
GovernorHelper.proposalStatesToBitMap([ProposalState.Queued]), // proposal needs queueing
);
});

Expand All @@ -628,7 +670,7 @@ describe('GovernorTimelockAccess', function () {
.withArgs(
this.proposal.id,
ProposalState.Canceled,
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded]), // not queueing necessary
);
});

Expand All @@ -649,7 +691,7 @@ describe('GovernorTimelockAccess', function () {
.withArgs(
this.proposal.id,
ProposalState.Canceled,
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded]), // not queueing necessary
);
});

Expand Down
20 changes: 11 additions & 9 deletions test/governance/extensions/GovernorTimelockCompound.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ describe('GovernorTimelockCompound', function () {
.to.be.revertedWithCustomError(this.mock, 'GovernorAlreadyQueuedProposal')
.withArgs(id);
await expect(this.helper.execute())
.to.be.revertedWithCustomError(this.mock, 'GovernorNotQueuedProposal')
.withArgs(id);
.to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
.withArgs(id, ProposalState.Succeeded, GovernorHelper.proposalStatesToBitMap([ProposalState.Queued]));
});
});

Expand All @@ -168,11 +168,13 @@ describe('GovernorTimelockCompound', function () {
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
await this.helper.waitForDeadline(1n);

expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Succeeded);

await expect(this.helper.execute())
.to.be.revertedWithCustomError(this.mock, 'GovernorNotQueuedProposal')
.withArgs(this.proposal.id);
.to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
.withArgs(
this.proposal.id,
ProposalState.Succeeded,
GovernorHelper.proposalStatesToBitMap([ProposalState.Queued]),
);
});

it('if too early', async function () {
Expand Down Expand Up @@ -204,7 +206,7 @@ describe('GovernorTimelockCompound', function () {
.withArgs(
this.proposal.id,
ProposalState.Expired,
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
GovernorHelper.proposalStatesToBitMap([ProposalState.Queued]),
);
});

Expand All @@ -222,7 +224,7 @@ describe('GovernorTimelockCompound', function () {
.withArgs(
this.proposal.id,
ProposalState.Executed,
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
GovernorHelper.proposalStatesToBitMap([ProposalState.Queued]),
);
});
});
Expand Down Expand Up @@ -317,7 +319,7 @@ describe('GovernorTimelockCompound', function () {
.withArgs(
this.proposal.id,
ProposalState.Canceled,
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
GovernorHelper.proposalStatesToBitMap([ProposalState.Queued]),
);
});
});
Expand Down
16 changes: 9 additions & 7 deletions test/governance/extensions/GovernorTimelockControl.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,13 @@ describe('GovernorTimelockControl', function () {
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
await this.helper.waitForDeadline(1n);

expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Succeeded);

await expect(this.helper.execute())
.to.be.revertedWithCustomError(this.timelock, 'TimelockUnexpectedOperationState')
.withArgs(this.proposal.timelockid, GovernorHelper.proposalStatesToBitMap(OperationState.Ready));
.to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
.withArgs(
this.proposal.id,
ProposalState.Succeeded,
GovernorHelper.proposalStatesToBitMap([ProposalState.Queued]),
);
});

it('if too early', async function () {
Expand Down Expand Up @@ -201,7 +203,7 @@ describe('GovernorTimelockControl', function () {
.withArgs(
this.proposal.id,
ProposalState.Executed,
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
GovernorHelper.proposalStatesToBitMap([ProposalState.Queued]),
);
});

Expand All @@ -224,7 +226,7 @@ describe('GovernorTimelockControl', function () {
.withArgs(
this.proposal.id,
ProposalState.Executed,
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
GovernorHelper.proposalStatesToBitMap([ProposalState.Queued]),
);
});
});
Expand Down Expand Up @@ -270,7 +272,7 @@ describe('GovernorTimelockControl', function () {
.withArgs(
this.proposal.id,
ProposalState.Canceled,
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
GovernorHelper.proposalStatesToBitMap([ProposalState.Queued]),
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ describe('GovernorVotesQuorumFraction', function () {
.withArgs(
this.proposal.id,
ProposalState.Defeated,
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded]),
);
});

Expand Down