Add a governor module to protect against late quorum (#2973)
Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
This commit is contained in:
@ -1,5 +1,8 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
* `GovernorExtendedVoting`: add new module to ensure a minimum voting duration is available after the quorum is reached. ([#2973](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2973))
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
* `GovernorTimelockControl`: improve the `state()` function to have it reflect cases where a proposal has been canceled directly on the timelock. ([#2977](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2977))
|
* `GovernorTimelockControl`: improve the `state()` function to have it reflect cases where a proposal has been canceled directly on the timelock. ([#2977](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2977))
|
||||||
|
|||||||
@ -110,24 +110,37 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor {
|
|||||||
* @dev See {IGovernor-state}.
|
* @dev See {IGovernor-state}.
|
||||||
*/
|
*/
|
||||||
function state(uint256 proposalId) public view virtual override returns (ProposalState) {
|
function state(uint256 proposalId) public view virtual override returns (ProposalState) {
|
||||||
ProposalCore memory proposal = _proposals[proposalId];
|
ProposalCore storage proposal = _proposals[proposalId];
|
||||||
|
|
||||||
if (proposal.executed) {
|
if (proposal.executed) {
|
||||||
return ProposalState.Executed;
|
return ProposalState.Executed;
|
||||||
} else if (proposal.canceled) {
|
}
|
||||||
|
|
||||||
|
if (proposal.canceled) {
|
||||||
return ProposalState.Canceled;
|
return ProposalState.Canceled;
|
||||||
} else if (proposal.voteStart.getDeadline() >= block.number) {
|
}
|
||||||
return ProposalState.Pending;
|
|
||||||
} else if (proposal.voteEnd.getDeadline() >= block.number) {
|
uint256 snapshot = proposalSnapshot(proposalId);
|
||||||
return ProposalState.Active;
|
|
||||||
} else if (proposal.voteEnd.isExpired()) {
|
if (snapshot == 0) {
|
||||||
return
|
|
||||||
_quorumReached(proposalId) && _voteSucceeded(proposalId)
|
|
||||||
? ProposalState.Succeeded
|
|
||||||
: ProposalState.Defeated;
|
|
||||||
} else {
|
|
||||||
revert("Governor: unknown proposal id");
|
revert("Governor: unknown proposal id");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (snapshot >= block.number) {
|
||||||
|
return ProposalState.Pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint256 deadline = proposalDeadline(proposalId);
|
||||||
|
|
||||||
|
if (deadline >= block.number) {
|
||||||
|
return ProposalState.Active;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_quorumReached(proposalId) && _voteSucceeded(proposalId)) {
|
||||||
|
return ProposalState.Succeeded;
|
||||||
|
} else {
|
||||||
|
return ProposalState.Defeated;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -42,6 +42,8 @@ Other extensions can customize the behavior or interface in multiple ways.
|
|||||||
|
|
||||||
* {GovernorSettings}: Manages some of the settings (voting delay, voting period duration, and proposal threshold) in a way that can be updated through a governance proposal, without requiering an upgrade.
|
* {GovernorSettings}: Manages some of the settings (voting delay, voting period duration, and proposal threshold) in a way that can be updated through a governance proposal, without requiering an upgrade.
|
||||||
|
|
||||||
|
* {GovernorPreventLateQuorum}: Ensures there is a minimum voting period after quorum is reached as a security protection against large voters.
|
||||||
|
|
||||||
In addition to modules and extensions, the core contract requires a few virtual functions to be implemented to your particular specifications:
|
In addition to modules and extensions, the core contract requires a few virtual functions to be implemented to your particular specifications:
|
||||||
|
|
||||||
* <<Governor-votingDelay-,`votingDelay()`>>: Delay (in number of blocks) since the proposal is submitted until voting power is fixed and voting starts. This can be used to enforce a delay after a proposal is published for users to buy tokens, or delegate their votes.
|
* <<Governor-votingDelay-,`votingDelay()`>>: Delay (in number of blocks) since the proposal is submitted until voting power is fixed and voting starts. This can be used to enforce a delay after a proposal is published for users to buy tokens, or delegate their votes.
|
||||||
@ -74,6 +76,8 @@ NOTE: Functions of the `Governor` contract do not include access control. If you
|
|||||||
|
|
||||||
{{GovernorSettings}}
|
{{GovernorSettings}}
|
||||||
|
|
||||||
|
{{GovernorPreventLateQuorum}}
|
||||||
|
|
||||||
{{GovernorCompatibilityBravo}}
|
{{GovernorCompatibilityBravo}}
|
||||||
|
|
||||||
=== Deprecated
|
=== Deprecated
|
||||||
|
|||||||
106
contracts/governance/extensions/GovernorPreventLateQuorum.sol
Normal file
106
contracts/governance/extensions/GovernorPreventLateQuorum.sol
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.8.0;
|
||||||
|
|
||||||
|
import "../Governor.sol";
|
||||||
|
import "../../utils/math/Math.sol";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev A module that ensures there is a minimum voting period after quorum is reached. This prevents a large voter from
|
||||||
|
* swaying a vote and triggering quorum at the last minute, by ensuring there is always time for other voters to react
|
||||||
|
* and try to oppose the decision.
|
||||||
|
*
|
||||||
|
* If a vote causes quorum to be reached, the proposal's voting period may be extended so that it does not end before at
|
||||||
|
* least a given number of blocks have passed (the "vote extension" parameter). This parameter can be set by the
|
||||||
|
* governance executor (e.g. through a governance proposal).
|
||||||
|
*
|
||||||
|
* _Available since v4.5._
|
||||||
|
*/
|
||||||
|
abstract contract GovernorPreventLateQuorum is Governor {
|
||||||
|
using SafeCast for uint256;
|
||||||
|
using Timers for Timers.BlockNumber;
|
||||||
|
|
||||||
|
uint64 private _voteExtension;
|
||||||
|
mapping(uint256 => Timers.BlockNumber) private _extendedDeadlines;
|
||||||
|
|
||||||
|
/// @dev Emitted when a proposal deadline is pushed back due to reaching quorum late in its voting period.
|
||||||
|
event ProposalExtended(uint256 indexed proposalId, uint64 extendedDeadline);
|
||||||
|
|
||||||
|
/// @dev Emitted when the {lateQuorumVoteExtension} parameter is changed.
|
||||||
|
event LateQuorumVoteExtensionSet(uint64 oldVoteExtension, uint64 newVoteExtension);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Initializes the vote extension parameter: the number of blocks that are required to pass since a proposal
|
||||||
|
* reaches quorum until its voting period ends. If necessary the voting period will be extended beyond the one set
|
||||||
|
* at proposal creation.
|
||||||
|
*/
|
||||||
|
constructor(uint64 initialVoteExtension) {
|
||||||
|
_setLateQuorumVoteExtension(initialVoteExtension);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Returns the proposal deadline, which may have been extended beyond that set at proposal creation, if the
|
||||||
|
* proposal reached quorum late in the voting period. See {Governor-proposalDeadline}.
|
||||||
|
*/
|
||||||
|
function proposalDeadline(uint256 proposalId) public view virtual override returns (uint256) {
|
||||||
|
return Math.max(super.proposalDeadline(proposalId), _extendedDeadlines[proposalId].getDeadline());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Casts a vote and detects if it caused quorum to be reached, potentially extending the voting period. See
|
||||||
|
* {Governor-_castVote}.
|
||||||
|
*
|
||||||
|
* May emit a {ProposalExtended} event.
|
||||||
|
*/
|
||||||
|
function _castVote(
|
||||||
|
uint256 proposalId,
|
||||||
|
address account,
|
||||||
|
uint8 support,
|
||||||
|
string memory reason
|
||||||
|
) internal virtual override returns (uint256) {
|
||||||
|
uint256 result = super._castVote(proposalId, account, support, reason);
|
||||||
|
|
||||||
|
Timers.BlockNumber storage extendedDeadline = _extendedDeadlines[proposalId];
|
||||||
|
|
||||||
|
if (extendedDeadline.isUnset() && _quorumReached(proposalId)) {
|
||||||
|
uint64 extendedDeadlineValue = block.number.toUint64() + lateQuorumVoteExtension();
|
||||||
|
|
||||||
|
if (extendedDeadlineValue > proposalDeadline(proposalId)) {
|
||||||
|
emit ProposalExtended(proposalId, extendedDeadlineValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
extendedDeadline.setDeadline(extendedDeadlineValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Returns the current value of the vote extension parameter: the number of blocks that are required to pass
|
||||||
|
* from the time a proposal reaches quorum until its voting period ends.
|
||||||
|
*/
|
||||||
|
function lateQuorumVoteExtension() public view virtual returns (uint64) {
|
||||||
|
return _voteExtension;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Changes the {lateQuorumVoteExtension}. This operation can only be performed by the governance executor,
|
||||||
|
* generally through a governance proposal.
|
||||||
|
*
|
||||||
|
* Emits a {LateQuorumVoteExtensionSet} event.
|
||||||
|
*/
|
||||||
|
function setLateQuorumVoteExtension(uint64 newVoteExtension) public virtual onlyGovernance {
|
||||||
|
_setLateQuorumVoteExtension(newVoteExtension);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Changes the {lateQuorumVoteExtension}. This is an internal function that can be exposed in a public function
|
||||||
|
* like {setLateQuorumVoteExtension} if another access control mechanism is needed.
|
||||||
|
*
|
||||||
|
* Emits a {LateQuorumVoteExtensionSet} event.
|
||||||
|
*/
|
||||||
|
function _setLateQuorumVoteExtension(uint64 newVoteExtension) internal virtual {
|
||||||
|
emit LateQuorumVoteExtensionSet(_voteExtension, newVoteExtension);
|
||||||
|
_voteExtension = newVoteExtension;
|
||||||
|
}
|
||||||
|
}
|
||||||
60
contracts/mocks/GovernorPreventLateQuorumMock.sol
Normal file
60
contracts/mocks/GovernorPreventLateQuorumMock.sol
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.8.0;
|
||||||
|
|
||||||
|
import "../governance/extensions/GovernorPreventLateQuorum.sol";
|
||||||
|
import "../governance/extensions/GovernorSettings.sol";
|
||||||
|
import "../governance/extensions/GovernorCountingSimple.sol";
|
||||||
|
import "../governance/extensions/GovernorVotes.sol";
|
||||||
|
|
||||||
|
contract GovernorPreventLateQuorumMock is
|
||||||
|
GovernorSettings,
|
||||||
|
GovernorVotes,
|
||||||
|
GovernorCountingSimple,
|
||||||
|
GovernorPreventLateQuorum
|
||||||
|
{
|
||||||
|
uint256 private _quorum;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
string memory name_,
|
||||||
|
ERC20Votes token_,
|
||||||
|
uint256 votingDelay_,
|
||||||
|
uint256 votingPeriod_,
|
||||||
|
uint256 quorum_,
|
||||||
|
uint64 voteExtension_
|
||||||
|
)
|
||||||
|
Governor(name_)
|
||||||
|
GovernorSettings(votingDelay_, votingPeriod_, 0)
|
||||||
|
GovernorVotes(token_)
|
||||||
|
GovernorPreventLateQuorum(voteExtension_)
|
||||||
|
{
|
||||||
|
_quorum = quorum_;
|
||||||
|
}
|
||||||
|
|
||||||
|
function quorum(uint256) public view virtual override returns (uint256) {
|
||||||
|
return _quorum;
|
||||||
|
}
|
||||||
|
|
||||||
|
function proposalDeadline(uint256 proposalId)
|
||||||
|
public
|
||||||
|
view
|
||||||
|
virtual
|
||||||
|
override(Governor, GovernorPreventLateQuorum)
|
||||||
|
returns (uint256)
|
||||||
|
{
|
||||||
|
return super.proposalDeadline(proposalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function proposalThreshold() public view virtual override(Governor, GovernorSettings) returns (uint256) {
|
||||||
|
return super.proposalThreshold();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _castVote(
|
||||||
|
uint256 proposalId,
|
||||||
|
address account,
|
||||||
|
uint8 support,
|
||||||
|
string memory reason
|
||||||
|
) internal virtual override(Governor, GovernorPreventLateQuorum) returns (uint256) {
|
||||||
|
return super._castVote(proposalId, account, support, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -65,7 +65,7 @@ function runGovernorWorkflow () {
|
|||||||
// vote
|
// vote
|
||||||
if (tryGet(this.settings, 'voters')) {
|
if (tryGet(this.settings, 'voters')) {
|
||||||
this.receipts.castVote = [];
|
this.receipts.castVote = [];
|
||||||
for (const voter of this.settings.voters) {
|
for (const voter of this.settings.voters.filter(({ support }) => !!support)) {
|
||||||
if (!voter.signature) {
|
if (!voter.signature) {
|
||||||
this.receipts.castVote.push(
|
this.receipts.castVote.push(
|
||||||
await getReceiptOrRevert(
|
await getReceiptOrRevert(
|
||||||
|
|||||||
247
test/governance/extensions/GovernorPreventLateQuorum.test.js
Normal file
247
test/governance/extensions/GovernorPreventLateQuorum.test.js
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
const { BN, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
|
||||||
|
const Enums = require('../../helpers/enums');
|
||||||
|
|
||||||
|
const {
|
||||||
|
runGovernorWorkflow,
|
||||||
|
} = require('../GovernorWorkflow.behavior');
|
||||||
|
|
||||||
|
const Token = artifacts.require('ERC20VotesCompMock');
|
||||||
|
const Governor = artifacts.require('GovernorPreventLateQuorumMock');
|
||||||
|
const CallReceiver = artifacts.require('CallReceiverMock');
|
||||||
|
|
||||||
|
contract('GovernorPreventLateQuorum', function (accounts) {
|
||||||
|
const [ owner, proposer, voter1, voter2, voter3, voter4 ] = accounts;
|
||||||
|
|
||||||
|
const name = 'OZ-Governor';
|
||||||
|
// const version = '1';
|
||||||
|
const tokenName = 'MockToken';
|
||||||
|
const tokenSymbol = 'MTKN';
|
||||||
|
const tokenSupply = web3.utils.toWei('100');
|
||||||
|
const votingDelay = new BN(4);
|
||||||
|
const votingPeriod = new BN(16);
|
||||||
|
const lateQuorumVoteExtension = new BN(8);
|
||||||
|
const quorum = web3.utils.toWei('1');
|
||||||
|
|
||||||
|
beforeEach(async function () {
|
||||||
|
this.owner = owner;
|
||||||
|
this.token = await Token.new(tokenName, tokenSymbol);
|
||||||
|
this.mock = await Governor.new(
|
||||||
|
name,
|
||||||
|
this.token.address,
|
||||||
|
votingDelay,
|
||||||
|
votingPeriod,
|
||||||
|
quorum,
|
||||||
|
lateQuorumVoteExtension,
|
||||||
|
);
|
||||||
|
this.receiver = await CallReceiver.new();
|
||||||
|
await this.token.mint(owner, tokenSupply);
|
||||||
|
await this.token.delegate(voter1, { from: voter1 });
|
||||||
|
await this.token.delegate(voter2, { from: voter2 });
|
||||||
|
await this.token.delegate(voter3, { from: voter3 });
|
||||||
|
await this.token.delegate(voter4, { from: voter4 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deployment check', async function () {
|
||||||
|
expect(await this.mock.name()).to.be.equal(name);
|
||||||
|
expect(await this.mock.token()).to.be.equal(this.token.address);
|
||||||
|
expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay);
|
||||||
|
expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
|
||||||
|
expect(await this.mock.quorum(0)).to.be.bignumber.equal(quorum);
|
||||||
|
expect(await this.mock.lateQuorumVoteExtension()).to.be.bignumber.equal(lateQuorumVoteExtension);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('nominal is unaffected', function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
this.settings = {
|
||||||
|
proposal: [
|
||||||
|
[ this.receiver.address ],
|
||||||
|
[ 0 ],
|
||||||
|
[ this.receiver.contract.methods.mockFunction().encodeABI() ],
|
||||||
|
'<proposal description>',
|
||||||
|
],
|
||||||
|
proposer,
|
||||||
|
tokenHolder: owner,
|
||||||
|
voters: [
|
||||||
|
{ voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For, reason: 'This is nice' },
|
||||||
|
{ voter: voter2, weight: web3.utils.toWei('7'), support: Enums.VoteType.For },
|
||||||
|
{ voter: voter3, weight: web3.utils.toWei('5'), support: Enums.VoteType.Against },
|
||||||
|
{ voter: voter4, weight: web3.utils.toWei('2'), support: Enums.VoteType.Abstain },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async function () {
|
||||||
|
expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false);
|
||||||
|
expect(await this.mock.hasVoted(this.id, voter1)).to.be.equal(true);
|
||||||
|
expect(await this.mock.hasVoted(this.id, voter2)).to.be.equal(true);
|
||||||
|
|
||||||
|
await this.mock.proposalVotes(this.id).then(result => {
|
||||||
|
for (const [key, value] of Object.entries(Enums.VoteType)) {
|
||||||
|
expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal(
|
||||||
|
Object.values(this.settings.voters).filter(({ support }) => support === value).reduce(
|
||||||
|
(acc, { weight }) => acc.add(new BN(weight)),
|
||||||
|
new BN('0'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const startBlock = new BN(this.receipts.propose.blockNumber).add(votingDelay);
|
||||||
|
const endBlock = new BN(this.receipts.propose.blockNumber).add(votingDelay).add(votingPeriod);
|
||||||
|
expect(await this.mock.proposalSnapshot(this.id)).to.be.bignumber.equal(startBlock);
|
||||||
|
expect(await this.mock.proposalDeadline(this.id)).to.be.bignumber.equal(endBlock);
|
||||||
|
|
||||||
|
expectEvent(
|
||||||
|
this.receipts.propose,
|
||||||
|
'ProposalCreated',
|
||||||
|
{
|
||||||
|
proposalId: this.id,
|
||||||
|
proposer,
|
||||||
|
targets: this.settings.proposal[0],
|
||||||
|
// values: this.settings.proposal[1].map(value => new BN(value)),
|
||||||
|
signatures: this.settings.proposal[2].map(() => ''),
|
||||||
|
calldatas: this.settings.proposal[2],
|
||||||
|
startBlock,
|
||||||
|
endBlock,
|
||||||
|
description: this.settings.proposal[3],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.receipts.castVote.filter(Boolean).forEach(vote => {
|
||||||
|
const { voter } = vote.logs.find(Boolean).args;
|
||||||
|
expectEvent(
|
||||||
|
vote,
|
||||||
|
'VoteCast',
|
||||||
|
this.settings.voters.find(({ address }) => address === voter),
|
||||||
|
);
|
||||||
|
expectEvent.notEmitted(
|
||||||
|
vote,
|
||||||
|
'ProposalExtended',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expectEvent(
|
||||||
|
this.receipts.execute,
|
||||||
|
'ProposalExecuted',
|
||||||
|
{ proposalId: this.id },
|
||||||
|
);
|
||||||
|
await expectEvent.inTransaction(
|
||||||
|
this.receipts.execute.transactionHash,
|
||||||
|
this.receiver,
|
||||||
|
'MockFunctionCalled',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
runGovernorWorkflow();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Delay is extended to prevent last minute take-over', function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
this.settings = {
|
||||||
|
proposal: [
|
||||||
|
[ this.receiver.address ],
|
||||||
|
[ 0 ],
|
||||||
|
[ this.receiver.contract.methods.mockFunction().encodeABI() ],
|
||||||
|
'<proposal description>',
|
||||||
|
],
|
||||||
|
proposer,
|
||||||
|
tokenHolder: owner,
|
||||||
|
voters: [
|
||||||
|
{ voter: voter1, weight: web3.utils.toWei('0.2'), support: Enums.VoteType.Against },
|
||||||
|
{ voter: voter2, weight: web3.utils.toWei('1.0') }, // do not actually vote, only getting tokens
|
||||||
|
{ voter: voter3, weight: web3.utils.toWei('0.9') }, // do not actually vote, only getting tokens
|
||||||
|
],
|
||||||
|
steps: {
|
||||||
|
wait: { enable: false },
|
||||||
|
execute: { enable: false },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async function () {
|
||||||
|
expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
|
||||||
|
|
||||||
|
const startBlock = new BN(this.receipts.propose.blockNumber).add(votingDelay);
|
||||||
|
const endBlock = new BN(this.receipts.propose.blockNumber).add(votingDelay).add(votingPeriod);
|
||||||
|
expect(await this.mock.proposalSnapshot(this.id)).to.be.bignumber.equal(startBlock);
|
||||||
|
expect(await this.mock.proposalDeadline(this.id)).to.be.bignumber.equal(endBlock);
|
||||||
|
|
||||||
|
// wait until the vote is almost over
|
||||||
|
await time.advanceBlockTo(endBlock.subn(1));
|
||||||
|
expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
|
||||||
|
|
||||||
|
// try to overtake the vote at the last minute
|
||||||
|
const tx = await this.mock.castVote(this.id, Enums.VoteType.For, { from: voter2 });
|
||||||
|
|
||||||
|
// vote duration is extended
|
||||||
|
const extendedBlock = new BN(tx.receipt.blockNumber).add(lateQuorumVoteExtension);
|
||||||
|
expect(await this.mock.proposalDeadline(this.id)).to.be.bignumber.equal(extendedBlock);
|
||||||
|
|
||||||
|
expectEvent(
|
||||||
|
tx,
|
||||||
|
'ProposalExtended',
|
||||||
|
{ proposalId: this.id, extendedDeadline: extendedBlock },
|
||||||
|
);
|
||||||
|
|
||||||
|
// vote is still active after expected end
|
||||||
|
await time.advanceBlockTo(endBlock.addn(1));
|
||||||
|
expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
|
||||||
|
|
||||||
|
// Still possible to vote
|
||||||
|
await this.mock.castVote(this.id, Enums.VoteType.Against, { from: voter3 });
|
||||||
|
|
||||||
|
// proposal fails
|
||||||
|
await time.advanceBlockTo(extendedBlock.addn(1));
|
||||||
|
expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated);
|
||||||
|
});
|
||||||
|
runGovernorWorkflow();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setLateQuorumVoteExtension', function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
this.newVoteExtension = new BN(0); // disable voting delay extension
|
||||||
|
});
|
||||||
|
|
||||||
|
it('protected', async function () {
|
||||||
|
await expectRevert(
|
||||||
|
this.mock.setLateQuorumVoteExtension(this.newVoteExtension),
|
||||||
|
'Governor: onlyGovernance',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('using workflow', function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
this.settings = {
|
||||||
|
proposal: [
|
||||||
|
[ this.mock.address ],
|
||||||
|
[ web3.utils.toWei('0') ],
|
||||||
|
[ this.mock.contract.methods.setLateQuorumVoteExtension(this.newVoteExtension).encodeABI() ],
|
||||||
|
'<proposal description>',
|
||||||
|
],
|
||||||
|
proposer,
|
||||||
|
tokenHolder: owner,
|
||||||
|
voters: [
|
||||||
|
{ voter: voter1, weight: web3.utils.toWei('1.0'), support: Enums.VoteType.For },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
afterEach(async function () {
|
||||||
|
expectEvent(
|
||||||
|
this.receipts.propose,
|
||||||
|
'ProposalCreated',
|
||||||
|
{ proposalId: this.id },
|
||||||
|
);
|
||||||
|
expectEvent(
|
||||||
|
this.receipts.execute,
|
||||||
|
'ProposalExecuted',
|
||||||
|
{ proposalId: this.id },
|
||||||
|
);
|
||||||
|
expectEvent(
|
||||||
|
this.receipts.execute,
|
||||||
|
'LateQuorumVoteExtensionSet',
|
||||||
|
{ oldVoteExtension: lateQuorumVoteExtension, newVoteExtension: this.newVoteExtension },
|
||||||
|
);
|
||||||
|
expect(await this.mock.lateQuorumVoteExtension()).to.be.bignumber.equal(this.newVoteExtension);
|
||||||
|
});
|
||||||
|
runGovernorWorkflow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user