-
Notifications
You must be signed in to change notification settings - Fork 11.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
286 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.0; | ||
|
||
import "../governance/extensions/GovernorExtendedVoting.sol"; | ||
import "../governance/extensions/GovernorSettings.sol"; | ||
import "../governance/extensions/GovernorCountingSimple.sol"; | ||
import "../governance/extensions/GovernorVotes.sol"; | ||
|
||
contract GovernorExtendedVotingMock is GovernorSettings, GovernorVotes, GovernorCountingSimple, GovernorExtendedVoting { | ||
uint256 private _quorum; | ||
|
||
constructor( | ||
string memory name_, | ||
ERC20Votes token_, | ||
uint256 votingDelay_, | ||
uint256 votingPeriod_, | ||
uint256 quorum_, | ||
uint64 votingDelayExtention_ | ||
) | ||
Governor(name_) | ||
GovernorSettings(votingDelay_, votingPeriod_, 0) | ||
GovernorVotes(token_) | ||
GovernorExtendedVoting(votingDelayExtention_) | ||
{ | ||
_quorum = quorum_; | ||
} | ||
|
||
function quorum(uint256) public view virtual override returns (uint256) { | ||
return _quorum; | ||
} | ||
|
||
function proposalDeadline(uint256 proposalId) | ||
public | ||
view | ||
virtual | ||
override(Governor, GovernorExtendedVoting) | ||
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, GovernorExtendedVoting) returns (uint256) { | ||
return super._castVote(proposalId, account, support, reason); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
230 changes: 230 additions & 0 deletions
230
test/governance/extensions/GovernorExtendedVoting.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,230 @@ | ||
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('GovernorExtendedVotingMock'); | ||
const CallReceiver = artifacts.require('CallReceiverMock'); | ||
|
||
contract('GovernorExtendedVoting', 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 votingDelayExtention = 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, votingDelayExtention); | ||
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.votingDelayExtention()).to.be.bignumber.equal(votingDelayExtention); | ||
}); | ||
|
||
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( | ||
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(votingDelayExtention); | ||
expect(await this.mock.proposalDeadline(this.id)).to.be.bignumber.equal(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('setVotingDelayExtention', function () { | ||
beforeEach(async function () { | ||
this.newVotingDelayExtention = new BN(0); // disable voting delay extention | ||
}); | ||
|
||
it('protected', async function () { | ||
await expectRevert( | ||
this.mock.setVotingDelayExtention(this.newVotingDelayExtention), | ||
'Governor: onlyGovernance', | ||
); | ||
}); | ||
|
||
describe('using workflow', function () { | ||
beforeEach(async function () { | ||
this.settings = { | ||
proposal: [ | ||
[ this.mock.address ], | ||
[ web3.utils.toWei('0') ], | ||
[ this.mock.contract.methods.setVotingDelayExtention(this.newVotingDelayExtention).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, | ||
'VotingDelayExtentionSet', | ||
{ oldVotingDelayExtention: votingDelayExtention, newVotingDelayExtention: this.newVotingDelayExtention }, | ||
); | ||
expect(await this.mock.votingDelayExtention()).to.be.bignumber.equal(this.newVotingDelayExtention); | ||
}); | ||
runGovernorWorkflow(); | ||
}); | ||
}); | ||
}); |