Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a public Governor.cancel function #3983

Merged
merged 9 commits into from
Jan 26, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

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

`Governor`: add a public `cancel(uint256)` function.
30 changes: 27 additions & 3 deletions contracts/governance/Governor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
Timers.BlockNumber voteEnd;
bool executed;
bool canceled;
address proposer;
}

string private _name;
Expand Down Expand Up @@ -95,9 +96,11 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
return
interfaceId ==
(type(IGovernor).interfaceId ^
this.cancel.selector ^
this.castVoteWithReasonAndParams.selector ^
this.castVoteWithReasonAndParamsBySig.selector ^
this.getVotesWithParams.selector) ||
interfaceId == (type(IGovernor).interfaceId ^ this.cancel.selector) ||
interfaceId == type(IGovernor).interfaceId ||
interfaceId == type(IERC1155Receiver).interfaceId ||
super.supportsInterface(interfaceId);
Expand Down Expand Up @@ -248,8 +251,10 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
bytes[] memory calldatas,
string memory description
) public virtual override returns (uint256) {
address proposer = _msgSender();

require(
getVotes(_msgSender(), block.number - 1) >= proposalThreshold(),
getVotes(proposer, block.number - 1) >= proposalThreshold(),
"Governor: proposer votes below proposal threshold"
);

Expand All @@ -267,10 +272,11 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive

proposal.voteStart.setDeadline(snapshot);
proposal.voteEnd.setDeadline(deadline);
proposal.proposer = proposer;

emit ProposalCreated(
proposalId,
_msgSender(),
proposer,
targets,
values,
new string[](targets.length),
Expand Down Expand Up @@ -310,6 +316,15 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
return proposalId;
}

/**
* @dev See {IGovernor-cancel}.
*/
function cancel(uint256 proposalId) public virtual override {
require(state(proposalId) == ProposalState.Pending, "Governor: too late to cancel");
require(_msgSender() == _proposals[proposalId].proposer, "Governor: only proposer can cancel");
_cancel(proposalId);
}

/**
* @dev Internal execution mechanism. Can be overridden to implement different execution mechanism
*/
Expand Down Expand Up @@ -375,7 +390,16 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
bytes[] memory calldatas,
bytes32 descriptionHash
) internal virtual returns (uint256) {
uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);
return _cancel(hashProposal(targets, values, calldatas, descriptionHash));
}

/**
* @dev Internal cancel mechanism: locks up the proposal timer, preventing it from being re-submitted. Marks it as
* canceled to allow distinguishing it from executed proposals.
*
* Emits a {IGovernor-ProposalCanceled} event.
*/
function _cancel(uint256 proposalId) internal virtual returns (uint256) {
ProposalState status = state(proposalId);

require(
Expand Down
8 changes: 8 additions & 0 deletions contracts/governance/IGovernor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,14 @@ abstract contract IGovernor is IERC165 {
bytes32 descriptionHash
) public payable virtual returns (uint256 proposalId);

/**
* @dev Cancel a proposal. This is restricted to Pending proposal (before the vote starts) and is restricted to
* the proposal's proposer.
Comment on lines +220 to +221
Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't really accurate here. The specific logic depends on the modules, for example it's different between Governor and GovernorCompatibilityBravo, so it should be documented in Governor.

*
* Emits a {ProposalCanceled} event.
*/
function cancel(uint256 proposalId) public virtual;

/**
* @dev Cast a vote
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,20 +99,15 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp
);
}

function cancel(uint256 proposalId) public virtual override {
function cancel(uint256 proposalId) public virtual override(IGovernor, Governor) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Needs docs explaining the logic, also that it overrides and doesn't call super.cancel from the core Governor.

ProposalDetails storage details = _proposalDetails[proposalId];

require(
_msgSender() == details.proposer || getVotes(details.proposer, block.number - 1) < proposalThreshold(),
"GovernorBravo: proposer above threshold"
);

_cancel(
details.targets,
details.values,
_encodeCalldata(details.signatures, details.calldatas),
details.descriptionHash
);
_cancel(proposalId);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,6 @@ abstract contract IGovernorCompatibilityBravo is IGovernor {
*/
function execute(uint256 proposalId) public payable virtual;

/**
* @dev Cancels a proposal only if sender is the proposer, or proposer delegates dropped below proposal threshold.
*/
function cancel(uint256 proposalId) public virtual;

/**
* @dev Part of the Governor Bravo's interface: _"Gets actions of a proposal"_.
*/
Expand Down
4 changes: 4 additions & 0 deletions contracts/mocks/governance/GovernorCompatibilityBravoMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ abstract contract GovernorCompatibilityBravoMock is
return super.execute(targets, values, calldatas, salt);
}

function cancel(uint256 proposalId) public override(Governor, GovernorCompatibilityBravo, IGovernor) {
super.cancel(proposalId);
}

function _execute(
uint256 proposalId,
address[] memory targets,
Expand Down
4 changes: 4 additions & 0 deletions contracts/mocks/wizard/MyGovernor3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ contract MyGovernor is
return super.propose(targets, values, calldatas, description);
}

function cancel(uint256 proposalId) public override(Governor, GovernorCompatibilityBravo, IGovernor) {
super.cancel(proposalId);
}

function _execute(
uint256 proposalId,
address[] memory targets,
Expand Down
128 changes: 91 additions & 37 deletions test/governance/Governor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -382,55 +382,109 @@ contract('Governor', function (accounts) {
});

describe('cancel', function () {
it('before proposal', async function () {
await expectRevert(this.helper.cancel(), 'Governor: unknown proposal id');
});
describe('internal', function () {
it('before proposal', async function () {
await expectRevert(this.helper.cancel(), 'Governor: unknown proposal id');
});

it('after proposal', async function () {
await this.helper.propose();
it('after proposal', async function () {
await this.helper.propose();

await this.helper.cancel();
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
await this.helper.cancel();
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);

await this.helper.waitForSnapshot();
await expectRevert(
this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
'Governor: vote not currently active',
);
});
await this.helper.waitForSnapshot();
await expectRevert(
this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
'Governor: vote not currently active',
);
});

it('after vote', async function () {
await this.helper.propose();
await this.helper.waitForSnapshot();
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
it('after vote', async function () {
await this.helper.propose();
await this.helper.waitForSnapshot();
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });

await this.helper.cancel();
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
await this.helper.cancel();
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);

await this.helper.waitForDeadline();
await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
});
await this.helper.waitForDeadline();
await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
});

it('after deadline', async function () {
await this.helper.propose();
await this.helper.waitForSnapshot();
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
await this.helper.waitForDeadline();
it('after deadline', async function () {
await this.helper.propose();
await this.helper.waitForSnapshot();
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
await this.helper.waitForDeadline();

await this.helper.cancel();
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);

await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
});

await this.helper.cancel();
expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
it('after execution', async function () {
await this.helper.propose();
await this.helper.waitForSnapshot();
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
await this.helper.waitForDeadline();
await this.helper.execute();

await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
await expectRevert(this.helper.cancel(), 'Governor: proposal not active');
});
});

it('after execution', async function () {
await this.helper.propose();
await this.helper.waitForSnapshot();
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
await this.helper.waitForDeadline();
await this.helper.execute();
describe('public', function () {
it('before proposal', async function () {
await expectRevert(this.helper.cancel(true), 'Governor: unknown proposal id');
});

it('after proposal', async function () {
await this.helper.propose();

await this.helper.cancel(true);
});

it('after proposal - restricted to proposer', async function () {
await this.helper.propose();

await expectRevert(this.helper.cancel(true, { from: owner }), 'Governor: only proposer can cancel');
});

it('after vote started', async function () {
await this.helper.propose();
await this.helper.waitForSnapshot(1); // snapshot + 1 block

await expectRevert(this.helper.cancel(true), 'Governor: too late to cancel');
});

await expectRevert(this.helper.cancel(), 'Governor: proposal not active');
it('after vote', async function () {
await this.helper.propose();
await this.helper.waitForSnapshot();
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });

await expectRevert(this.helper.cancel(true), 'Governor: too late to cancel');
});

it('after deadline', async function () {
await this.helper.propose();
await this.helper.waitForSnapshot();
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
await this.helper.waitForDeadline();

await expectRevert(this.helper.cancel(true), 'Governor: too late to cancel');
});

it('after execution', async function () {
await this.helper.propose();
await this.helper.waitForSnapshot();
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
await this.helper.waitForDeadline();
await this.helper.execute();

await expectRevert(this.helper.cancel(true), 'Governor: too late to cancel');
});
});
});

Expand Down
4 changes: 2 additions & 2 deletions test/helpers/governance.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ class GovernorHelper {
);
}

cancel(opts = null) {
cancel(usePublic = false, opts = null) {
frangio marked this conversation as resolved.
Show resolved Hide resolved
const proposal = this.currentProposal;

return proposal.useCompatibilityInterface
return proposal.useCompatibilityInterface || usePublic
? this.governor.methods['cancel(uint256)'](...concatOpts([proposal.id], opts))
: this.governor.methods['$_cancel(address[],uint256[],bytes[],bytes32)'](
...concatOpts(proposal.shortProposal, opts),
Expand Down