dexorder
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { GovernorHelper } = require('../../helpers/governance');
|
||||
const { VoteType } = require('../../helpers/enums');
|
||||
|
||||
const TOKENS = [
|
||||
{ Token: '$ERC721Votes', mode: 'blocknumber' },
|
||||
{ Token: '$ERC721VotesTimestampMock', mode: 'timestamp' },
|
||||
];
|
||||
|
||||
const name = 'OZ-Governor';
|
||||
const version = '1';
|
||||
const tokenName = 'MockNFToken';
|
||||
const tokenSymbol = 'MTKN';
|
||||
const NFT0 = 0n;
|
||||
const NFT1 = 1n;
|
||||
const NFT2 = 2n;
|
||||
const NFT3 = 3n;
|
||||
const NFT4 = 4n;
|
||||
const votingDelay = 4n;
|
||||
const votingPeriod = 16n;
|
||||
const value = ethers.parseEther('1');
|
||||
|
||||
describe('GovernorERC721', function () {
|
||||
for (const { Token, mode } of TOKENS) {
|
||||
const fixture = async () => {
|
||||
const [owner, voter1, voter2, voter3, voter4] = await ethers.getSigners();
|
||||
const receiver = await ethers.deployContract('CallReceiverMock');
|
||||
|
||||
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]);
|
||||
const mock = await ethers.deployContract('$GovernorMock', [
|
||||
name, // name
|
||||
votingDelay, // initialVotingDelay
|
||||
votingPeriod, // initialVotingPeriod
|
||||
0n, // initialProposalThreshold
|
||||
token, // tokenAddress
|
||||
10n, // quorumNumeratorValue
|
||||
]);
|
||||
|
||||
await owner.sendTransaction({ to: mock, value });
|
||||
await Promise.all([NFT0, NFT1, NFT2, NFT3, NFT4].map(tokenId => token.$_mint(owner, tokenId)));
|
||||
|
||||
const helper = new GovernorHelper(mock, mode);
|
||||
await helper.connect(owner).delegate({ token, to: voter1, tokenId: NFT0 });
|
||||
await helper.connect(owner).delegate({ token, to: voter2, tokenId: NFT1 });
|
||||
await helper.connect(owner).delegate({ token, to: voter2, tokenId: NFT2 });
|
||||
await helper.connect(owner).delegate({ token, to: voter3, tokenId: NFT3 });
|
||||
await helper.connect(owner).delegate({ token, to: voter4, tokenId: NFT4 });
|
||||
|
||||
return {
|
||||
owner,
|
||||
voter1,
|
||||
voter2,
|
||||
voter3,
|
||||
voter4,
|
||||
receiver,
|
||||
token,
|
||||
mock,
|
||||
helper,
|
||||
};
|
||||
};
|
||||
|
||||
describe(`using ${Token}`, function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
// initiate fresh proposal
|
||||
this.proposal = this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.receiver.target,
|
||||
data: this.receiver.interface.encodeFunctionData('mockFunction'),
|
||||
value,
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
});
|
||||
|
||||
it('deployment check', async function () {
|
||||
expect(await this.mock.name()).to.equal(name);
|
||||
expect(await this.mock.token()).to.equal(this.token);
|
||||
expect(await this.mock.votingDelay()).to.equal(votingDelay);
|
||||
expect(await this.mock.votingPeriod()).to.equal(votingPeriod);
|
||||
expect(await this.mock.quorum(0n)).to.equal(0n);
|
||||
|
||||
expect(await this.token.getVotes(this.voter1)).to.equal(1n); // NFT0
|
||||
expect(await this.token.getVotes(this.voter2)).to.equal(2n); // NFT1 & NFT2
|
||||
expect(await this.token.getVotes(this.voter3)).to.equal(1n); // NFT3
|
||||
expect(await this.token.getVotes(this.voter4)).to.equal(1n); // NFT4
|
||||
});
|
||||
|
||||
it('voting with ERC721 token', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
|
||||
await expect(this.helper.connect(this.voter1).vote({ support: VoteType.For }))
|
||||
.to.emit(this.mock, 'VoteCast')
|
||||
.withArgs(this.voter1, this.proposal.id, VoteType.For, 1n, '');
|
||||
|
||||
await expect(this.helper.connect(this.voter2).vote({ support: VoteType.For }))
|
||||
.to.emit(this.mock, 'VoteCast')
|
||||
.withArgs(this.voter2, this.proposal.id, VoteType.For, 2n, '');
|
||||
|
||||
await expect(this.helper.connect(this.voter3).vote({ support: VoteType.Against }))
|
||||
.to.emit(this.mock, 'VoteCast')
|
||||
.withArgs(this.voter3, this.proposal.id, VoteType.Against, 1n, '');
|
||||
|
||||
await expect(this.helper.connect(this.voter4).vote({ support: VoteType.Abstain }))
|
||||
.to.emit(this.mock, 'VoteCast')
|
||||
.withArgs(this.voter4, this.proposal.id, VoteType.Abstain, 1n, '');
|
||||
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.execute();
|
||||
|
||||
expect(await this.mock.hasVoted(this.proposal.id, this.owner)).to.be.false;
|
||||
expect(await this.mock.hasVoted(this.proposal.id, this.voter1)).to.be.true;
|
||||
expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.be.true;
|
||||
expect(await this.mock.hasVoted(this.proposal.id, this.voter3)).to.be.true;
|
||||
expect(await this.mock.hasVoted(this.proposal.id, this.voter4)).to.be.true;
|
||||
|
||||
expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([
|
||||
1n, // againstVotes
|
||||
3n, // forVotes
|
||||
1n, // abstainVotes
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,185 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { GovernorHelper } = require('../../helpers/governance');
|
||||
const { ProposalState, VoteType } = require('../../helpers/enums');
|
||||
const time = require('../../helpers/time');
|
||||
|
||||
const TOKENS = [
|
||||
{ Token: '$ERC20Votes', mode: 'blocknumber' },
|
||||
{ Token: '$ERC20VotesTimestampMock', mode: 'timestamp' },
|
||||
];
|
||||
|
||||
const name = 'OZ-Governor';
|
||||
const version = '1';
|
||||
const tokenName = 'MockToken';
|
||||
const tokenSymbol = 'MTKN';
|
||||
const tokenSupply = ethers.parseEther('100');
|
||||
const votingDelay = 4n;
|
||||
const votingPeriod = 16n;
|
||||
const lateQuorumVoteExtension = 8n;
|
||||
const quorum = ethers.parseEther('1');
|
||||
const value = ethers.parseEther('1');
|
||||
|
||||
describe('GovernorPreventLateQuorum', function () {
|
||||
for (const { Token, mode } of TOKENS) {
|
||||
const fixture = async () => {
|
||||
const [owner, proposer, voter1, voter2, voter3, voter4] = await ethers.getSigners();
|
||||
const receiver = await ethers.deployContract('CallReceiverMock');
|
||||
|
||||
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]);
|
||||
const mock = await ethers.deployContract('$GovernorPreventLateQuorumMock', [
|
||||
name, // name
|
||||
votingDelay, // initialVotingDelay
|
||||
votingPeriod, // initialVotingPeriod
|
||||
0n, // initialProposalThreshold
|
||||
token, // tokenAddress
|
||||
lateQuorumVoteExtension,
|
||||
quorum,
|
||||
]);
|
||||
|
||||
await owner.sendTransaction({ to: mock, value });
|
||||
await token.$_mint(owner, tokenSupply);
|
||||
|
||||
const helper = new GovernorHelper(mock, mode);
|
||||
await helper.connect(owner).delegate({ token, to: voter1, value: ethers.parseEther('10') });
|
||||
await helper.connect(owner).delegate({ token, to: voter2, value: ethers.parseEther('7') });
|
||||
await helper.connect(owner).delegate({ token, to: voter3, value: ethers.parseEther('5') });
|
||||
await helper.connect(owner).delegate({ token, to: voter4, value: ethers.parseEther('2') });
|
||||
|
||||
return { owner, proposer, voter1, voter2, voter3, voter4, receiver, token, mock, helper };
|
||||
};
|
||||
|
||||
describe(`using ${Token}`, function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
// initiate fresh proposal
|
||||
this.proposal = this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.receiver.target,
|
||||
data: this.receiver.interface.encodeFunctionData('mockFunction'),
|
||||
value,
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
});
|
||||
|
||||
it('deployment check', async function () {
|
||||
expect(await this.mock.name()).to.equal(name);
|
||||
expect(await this.mock.token()).to.equal(this.token);
|
||||
expect(await this.mock.votingDelay()).to.equal(votingDelay);
|
||||
expect(await this.mock.votingPeriod()).to.equal(votingPeriod);
|
||||
expect(await this.mock.quorum(0)).to.equal(quorum);
|
||||
expect(await this.mock.lateQuorumVoteExtension()).to.equal(lateQuorumVoteExtension);
|
||||
});
|
||||
|
||||
it('nominal workflow unaffected', async function () {
|
||||
const txPropose = await this.helper.connect(this.proposer).propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.connect(this.voter2).vote({ support: VoteType.For });
|
||||
await this.helper.connect(this.voter3).vote({ support: VoteType.Against });
|
||||
await this.helper.connect(this.voter4).vote({ support: VoteType.Abstain });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.execute();
|
||||
|
||||
expect(await this.mock.hasVoted(this.proposal.id, this.owner)).to.be.false;
|
||||
expect(await this.mock.hasVoted(this.proposal.id, this.voter1)).to.be.true;
|
||||
expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.be.true;
|
||||
expect(await this.mock.hasVoted(this.proposal.id, this.voter3)).to.be.true;
|
||||
expect(await this.mock.hasVoted(this.proposal.id, this.voter4)).to.be.true;
|
||||
|
||||
expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([
|
||||
ethers.parseEther('5'), // againstVotes
|
||||
ethers.parseEther('17'), // forVotes
|
||||
ethers.parseEther('2'), // abstainVotes
|
||||
]);
|
||||
|
||||
const voteStart = (await time.clockFromReceipt[mode](txPropose)) + votingDelay;
|
||||
const voteEnd = (await time.clockFromReceipt[mode](txPropose)) + votingDelay + votingPeriod;
|
||||
expect(await this.mock.proposalSnapshot(this.proposal.id)).to.equal(voteStart);
|
||||
expect(await this.mock.proposalDeadline(this.proposal.id)).to.equal(voteEnd);
|
||||
|
||||
await expect(txPropose)
|
||||
.to.emit(this.mock, 'ProposalCreated')
|
||||
.withArgs(
|
||||
this.proposal.id,
|
||||
this.proposer,
|
||||
this.proposal.targets,
|
||||
this.proposal.values,
|
||||
this.proposal.signatures,
|
||||
this.proposal.data,
|
||||
voteStart,
|
||||
voteEnd,
|
||||
this.proposal.description,
|
||||
);
|
||||
});
|
||||
|
||||
it('Delay is extended to prevent last minute take-over', async function () {
|
||||
const txPropose = await this.helper.connect(this.proposer).propose();
|
||||
|
||||
// compute original schedule
|
||||
const snapshotTimepoint = (await time.clockFromReceipt[mode](txPropose)) + votingDelay;
|
||||
const deadlineTimepoint = (await time.clockFromReceipt[mode](txPropose)) + votingDelay + votingPeriod;
|
||||
expect(await this.mock.proposalSnapshot(this.proposal.id)).to.equal(snapshotTimepoint);
|
||||
expect(await this.mock.proposalDeadline(this.proposal.id)).to.equal(deadlineTimepoint);
|
||||
// wait for the last minute to vote
|
||||
await this.helper.waitForDeadline(-1n);
|
||||
const txVote = await this.helper.connect(this.voter2).vote({ support: VoteType.For });
|
||||
|
||||
// cannot execute yet
|
||||
expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Active);
|
||||
|
||||
// compute new extended schedule
|
||||
const extendedDeadline = (await time.clockFromReceipt[mode](txVote)) + lateQuorumVoteExtension;
|
||||
expect(await this.mock.proposalSnapshot(this.proposal.id)).to.equal(snapshotTimepoint);
|
||||
expect(await this.mock.proposalDeadline(this.proposal.id)).to.equal(extendedDeadline);
|
||||
|
||||
// still possible to vote
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.Against });
|
||||
|
||||
await this.helper.waitForDeadline();
|
||||
expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Active);
|
||||
await this.helper.waitForDeadline(1n);
|
||||
expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Defeated);
|
||||
|
||||
// check extension event
|
||||
await expect(txVote).to.emit(this.mock, 'ProposalExtended').withArgs(this.proposal.id, extendedDeadline);
|
||||
});
|
||||
|
||||
describe('onlyGovernance updates', function () {
|
||||
it('setLateQuorumVoteExtension is protected', async function () {
|
||||
await expect(this.mock.connect(this.owner).setLateQuorumVoteExtension(0n))
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor')
|
||||
.withArgs(this.owner);
|
||||
});
|
||||
|
||||
it('can setLateQuorumVoteExtension through governance', async function () {
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.target,
|
||||
data: this.mock.interface.encodeFunctionData('setLateQuorumVoteExtension', [0n]),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
await expect(this.helper.execute())
|
||||
.to.emit(this.mock, 'LateQuorumVoteExtensionSet')
|
||||
.withArgs(lateQuorumVoteExtension, 0n);
|
||||
|
||||
expect(await this.mock.lateQuorumVoteExtension()).to.equal(0n);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,155 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs');
|
||||
const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');
|
||||
|
||||
const { GovernorHelper, timelockSalt } = require('../../helpers/governance');
|
||||
const { VoteType } = require('../../helpers/enums');
|
||||
|
||||
const TOKENS = [
|
||||
{ Token: '$ERC20Votes', mode: 'blocknumber' },
|
||||
{ Token: '$ERC20VotesTimestampMock', mode: 'timestamp' },
|
||||
];
|
||||
|
||||
const DEFAULT_ADMIN_ROLE = ethers.ZeroHash;
|
||||
const PROPOSER_ROLE = ethers.id('PROPOSER_ROLE');
|
||||
const EXECUTOR_ROLE = ethers.id('EXECUTOR_ROLE');
|
||||
const CANCELLER_ROLE = ethers.id('CANCELLER_ROLE');
|
||||
|
||||
const name = 'OZ-Governor';
|
||||
const version = '1';
|
||||
const tokenName = 'MockToken';
|
||||
const tokenSymbol = 'MTKN';
|
||||
const tokenSupply = ethers.parseEther('100');
|
||||
const votingDelay = 4n;
|
||||
const votingPeriod = 16n;
|
||||
const value = ethers.parseEther('1');
|
||||
const delay = 3600n;
|
||||
|
||||
describe('GovernorStorage', function () {
|
||||
for (const { Token, mode } of TOKENS) {
|
||||
const fixture = async () => {
|
||||
const [deployer, owner, proposer, voter1, voter2, voter3, voter4] = await ethers.getSigners();
|
||||
const receiver = await ethers.deployContract('CallReceiverMock');
|
||||
|
||||
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]);
|
||||
const timelock = await ethers.deployContract('TimelockController', [delay, [], [], deployer]);
|
||||
const mock = await ethers.deployContract('$GovernorStorageMock', [
|
||||
name,
|
||||
votingDelay,
|
||||
votingPeriod,
|
||||
0n,
|
||||
timelock,
|
||||
token,
|
||||
0n,
|
||||
]);
|
||||
|
||||
await owner.sendTransaction({ to: timelock, value });
|
||||
await token.$_mint(owner, tokenSupply);
|
||||
await timelock.grantRole(PROPOSER_ROLE, mock);
|
||||
await timelock.grantRole(PROPOSER_ROLE, owner);
|
||||
await timelock.grantRole(CANCELLER_ROLE, mock);
|
||||
await timelock.grantRole(CANCELLER_ROLE, owner);
|
||||
await timelock.grantRole(EXECUTOR_ROLE, ethers.ZeroAddress);
|
||||
await timelock.revokeRole(DEFAULT_ADMIN_ROLE, deployer);
|
||||
|
||||
const helper = new GovernorHelper(mock, mode);
|
||||
await helper.connect(owner).delegate({ token, to: voter1, value: ethers.parseEther('10') });
|
||||
await helper.connect(owner).delegate({ token, to: voter2, value: ethers.parseEther('7') });
|
||||
await helper.connect(owner).delegate({ token, to: voter3, value: ethers.parseEther('5') });
|
||||
await helper.connect(owner).delegate({ token, to: voter4, value: ethers.parseEther('2') });
|
||||
|
||||
return { deployer, owner, proposer, voter1, voter2, voter3, voter4, receiver, token, timelock, mock, helper };
|
||||
};
|
||||
|
||||
describe(`using ${Token}`, function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
// initiate fresh proposal
|
||||
this.proposal = this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.receiver.target,
|
||||
data: this.receiver.interface.encodeFunctionData('mockFunction'),
|
||||
value,
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
this.proposal.timelockid = await this.timelock.hashOperationBatch(
|
||||
...this.proposal.shortProposal.slice(0, 3),
|
||||
ethers.ZeroHash,
|
||||
timelockSalt(this.mock.target, this.proposal.shortProposal[3]),
|
||||
);
|
||||
});
|
||||
|
||||
describe('proposal indexing', function () {
|
||||
it('before propose', async function () {
|
||||
expect(await this.mock.proposalCount()).to.equal(0n);
|
||||
|
||||
await expect(this.mock.proposalDetailsAt(0n)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);
|
||||
|
||||
await expect(this.mock.proposalDetails(this.proposal.id))
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorNonexistentProposal')
|
||||
.withArgs(this.proposal.id);
|
||||
});
|
||||
|
||||
it('after propose', async function () {
|
||||
await this.helper.propose();
|
||||
|
||||
expect(await this.mock.proposalCount()).to.equal(1n);
|
||||
|
||||
expect(await this.mock.proposalDetailsAt(0n)).to.deep.equal([
|
||||
this.proposal.id,
|
||||
this.proposal.targets,
|
||||
this.proposal.values,
|
||||
this.proposal.data,
|
||||
this.proposal.descriptionHash,
|
||||
]);
|
||||
|
||||
expect(await this.mock.proposalDetails(this.proposal.id)).to.deep.equal([
|
||||
this.proposal.targets,
|
||||
this.proposal.values,
|
||||
this.proposal.data,
|
||||
this.proposal.descriptionHash,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('queue and execute by id', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.connect(this.voter2).vote({ support: VoteType.For });
|
||||
await this.helper.connect(this.voter3).vote({ support: VoteType.Against });
|
||||
await this.helper.connect(this.voter4).vote({ support: VoteType.Abstain });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
await expect(this.mock.queue(this.proposal.id))
|
||||
.to.emit(this.mock, 'ProposalQueued')
|
||||
.withArgs(this.proposal.id, anyValue)
|
||||
.to.emit(this.timelock, 'CallScheduled')
|
||||
.withArgs(this.proposal.timelockid, ...Array(6).fill(anyValue))
|
||||
.to.emit(this.timelock, 'CallSalt')
|
||||
.withArgs(this.proposal.timelockid, anyValue);
|
||||
|
||||
await this.helper.waitForEta();
|
||||
|
||||
await expect(this.mock.execute(this.proposal.id))
|
||||
.to.emit(this.mock, 'ProposalExecuted')
|
||||
.withArgs(this.proposal.id)
|
||||
.to.emit(this.timelock, 'CallExecuted')
|
||||
.withArgs(this.proposal.timelockid, ...Array(4).fill(anyValue))
|
||||
.to.emit(this.receiver, 'MockFunctionCalled');
|
||||
});
|
||||
|
||||
it('cancel by id', async function () {
|
||||
await this.helper.connect(this.proposer).propose();
|
||||
await expect(this.mock.connect(this.proposer).cancel(this.proposal.id))
|
||||
.to.emit(this.mock, 'ProposalCanceled')
|
||||
.withArgs(this.proposal.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,864 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs');
|
||||
|
||||
const { GovernorHelper } = require('../../helpers/governance');
|
||||
const { hashOperation } = require('../../helpers/access-manager');
|
||||
const { max } = require('../../helpers/math');
|
||||
const { selector } = require('../../helpers/methods');
|
||||
const { ProposalState, VoteType } = require('../../helpers/enums');
|
||||
const time = require('../../helpers/time');
|
||||
|
||||
function prepareOperation({ sender, target, value = 0n, data = '0x' }) {
|
||||
return {
|
||||
id: hashOperation(sender, target, data),
|
||||
operation: { target, value, data },
|
||||
selector: data.slice(0, 10).padEnd(10, '0'),
|
||||
};
|
||||
}
|
||||
|
||||
const TOKENS = [
|
||||
{ Token: '$ERC20Votes', mode: 'blocknumber' },
|
||||
{ Token: '$ERC20VotesTimestampMock', mode: 'timestamp' },
|
||||
];
|
||||
|
||||
const name = 'OZ-Governor';
|
||||
const version = '1';
|
||||
const tokenName = 'MockToken';
|
||||
const tokenSymbol = 'MTKN';
|
||||
const tokenSupply = ethers.parseEther('100');
|
||||
const votingDelay = 4n;
|
||||
const votingPeriod = 16n;
|
||||
const value = ethers.parseEther('1');
|
||||
|
||||
describe('GovernorTimelockAccess', function () {
|
||||
for (const { Token, mode } of TOKENS) {
|
||||
const fixture = async () => {
|
||||
const [admin, voter1, voter2, voter3, voter4, other] = await ethers.getSigners();
|
||||
|
||||
const manager = await ethers.deployContract('$AccessManager', [admin]);
|
||||
const receiver = await ethers.deployContract('$AccessManagedTarget', [manager]);
|
||||
|
||||
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]);
|
||||
const mock = await ethers.deployContract('$GovernorTimelockAccessMock', [
|
||||
name,
|
||||
votingDelay,
|
||||
votingPeriod,
|
||||
0n,
|
||||
manager,
|
||||
0n,
|
||||
token,
|
||||
0n,
|
||||
]);
|
||||
|
||||
await admin.sendTransaction({ to: mock, value });
|
||||
await token.$_mint(admin, tokenSupply);
|
||||
|
||||
const helper = new GovernorHelper(mock, mode);
|
||||
await helper.connect(admin).delegate({ token, to: voter1, value: ethers.parseEther('10') });
|
||||
await helper.connect(admin).delegate({ token, to: voter2, value: ethers.parseEther('7') });
|
||||
await helper.connect(admin).delegate({ token, to: voter3, value: ethers.parseEther('5') });
|
||||
await helper.connect(admin).delegate({ token, to: voter4, value: ethers.parseEther('2') });
|
||||
|
||||
return { admin, voter1, voter2, voter3, voter4, other, manager, receiver, token, mock, helper };
|
||||
};
|
||||
|
||||
describe(`using ${Token}`, function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
|
||||
// restricted proposal
|
||||
this.restricted = prepareOperation({
|
||||
sender: this.mock.target,
|
||||
target: this.receiver.target,
|
||||
data: this.receiver.interface.encodeFunctionData('fnRestricted'),
|
||||
});
|
||||
|
||||
this.unrestricted = prepareOperation({
|
||||
sender: this.mock.target,
|
||||
target: this.receiver.target,
|
||||
data: this.receiver.interface.encodeFunctionData('fnUnrestricted'),
|
||||
});
|
||||
|
||||
this.fallback = prepareOperation({
|
||||
sender: this.mock.target,
|
||||
target: this.receiver.target,
|
||||
data: '0x1234',
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts ether transfers', async function () {
|
||||
await this.admin.sendTransaction({ to: this.mock, value: 1n });
|
||||
});
|
||||
|
||||
it('post deployment check', async function () {
|
||||
expect(await this.mock.name()).to.equal(name);
|
||||
expect(await this.mock.token()).to.equal(this.token);
|
||||
expect(await this.mock.votingDelay()).to.equal(votingDelay);
|
||||
expect(await this.mock.votingPeriod()).to.equal(votingPeriod);
|
||||
expect(await this.mock.quorum(0n)).to.equal(0n);
|
||||
|
||||
expect(await this.mock.accessManager()).to.equal(this.manager);
|
||||
});
|
||||
|
||||
it('sets base delay (seconds)', async function () {
|
||||
const baseDelay = time.duration.hours(10n);
|
||||
|
||||
// Only through governance
|
||||
await expect(this.mock.connect(this.voter1).setBaseDelaySeconds(baseDelay))
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor')
|
||||
.withArgs(this.voter1);
|
||||
|
||||
this.proposal = await this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.target,
|
||||
data: this.mock.interface.encodeFunctionData('setBaseDelaySeconds', [baseDelay]),
|
||||
},
|
||||
],
|
||||
'descr',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
await expect(this.helper.execute()).to.emit(this.mock, 'BaseDelaySet').withArgs(0n, baseDelay);
|
||||
|
||||
expect(await this.mock.baseDelaySeconds()).to.equal(baseDelay);
|
||||
});
|
||||
|
||||
it('sets access manager ignored', async function () {
|
||||
const selectors = ['0x12345678', '0x87654321', '0xabcdef01'];
|
||||
|
||||
// Only through governance
|
||||
await expect(this.mock.connect(this.voter1).setAccessManagerIgnored(this.other, selectors, true))
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor')
|
||||
.withArgs(this.voter1);
|
||||
|
||||
// Ignore
|
||||
await this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.target,
|
||||
data: this.mock.interface.encodeFunctionData('setAccessManagerIgnored', [
|
||||
this.other.address,
|
||||
selectors,
|
||||
true,
|
||||
]),
|
||||
},
|
||||
],
|
||||
'descr',
|
||||
);
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
const ignoreReceipt = this.helper.execute();
|
||||
for (const selector of selectors) {
|
||||
await expect(ignoreReceipt)
|
||||
.to.emit(this.mock, 'AccessManagerIgnoredSet')
|
||||
.withArgs(this.other, selector, true);
|
||||
expect(await this.mock.isAccessManagerIgnored(this.other, selector)).to.be.true;
|
||||
}
|
||||
|
||||
// Unignore
|
||||
await this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.target,
|
||||
data: this.mock.interface.encodeFunctionData('setAccessManagerIgnored', [
|
||||
this.other.address,
|
||||
selectors,
|
||||
false,
|
||||
]),
|
||||
},
|
||||
],
|
||||
'descr',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
const unignoreReceipt = this.helper.execute();
|
||||
for (const selector of selectors) {
|
||||
await expect(unignoreReceipt)
|
||||
.to.emit(this.mock, 'AccessManagerIgnoredSet')
|
||||
.withArgs(this.other, selector, false);
|
||||
expect(await this.mock.isAccessManagerIgnored(this.other, selector)).to.be.false;
|
||||
}
|
||||
});
|
||||
|
||||
it('sets access manager ignored when target is the governor', async function () {
|
||||
const selectors = ['0x12345678', '0x87654321', '0xabcdef01'];
|
||||
|
||||
await this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.target,
|
||||
data: this.mock.interface.encodeFunctionData('setAccessManagerIgnored', [
|
||||
this.mock.target,
|
||||
selectors,
|
||||
true,
|
||||
]),
|
||||
},
|
||||
],
|
||||
'descr',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
const tx = this.helper.execute();
|
||||
for (const selector of selectors) {
|
||||
await expect(tx).to.emit(this.mock, 'AccessManagerIgnoredSet').withArgs(this.mock, selector, true);
|
||||
expect(await this.mock.isAccessManagerIgnored(this.mock, selector)).to.be.true;
|
||||
}
|
||||
});
|
||||
|
||||
it('does not need to queue proposals with no delay', async function () {
|
||||
const roleId = 1n;
|
||||
const executionDelay = 0n;
|
||||
const baseDelay = 0n;
|
||||
|
||||
// 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);
|
||||
|
||||
await this.helper.setProposal([this.restricted.operation], 'descr');
|
||||
await this.helper.propose();
|
||||
expect(await this.mock.proposalNeedsQueuing(this.helper.currentProposal.id)).to.be.false;
|
||||
});
|
||||
|
||||
it('needs to queue proposals with any delay', async function () {
|
||||
const roleId = 1n;
|
||||
const delays = [
|
||||
[time.duration.hours(1n), time.duration.hours(2n)],
|
||||
[time.duration.hours(2n), time.duration.hours(1n)],
|
||||
];
|
||||
|
||||
for (const [executionDelay, baseDelay] of delays) {
|
||||
// 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);
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
describe('execution plan', function () {
|
||||
it('returns plan for delayed operations', async function () {
|
||||
const roleId = 1n;
|
||||
const delays = [
|
||||
[time.duration.hours(1n), time.duration.hours(2n)],
|
||||
[time.duration.hours(2n), time.duration.hours(1n)],
|
||||
];
|
||||
|
||||
for (const [executionDelay, baseDelay] of delays) {
|
||||
// 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);
|
||||
|
||||
this.proposal = await this.helper.setProposal(
|
||||
[this.restricted.operation],
|
||||
`executionDelay=${executionDelay.toString()}}baseDelay=${baseDelay.toString()}}`,
|
||||
);
|
||||
await this.helper.propose();
|
||||
|
||||
expect(await this.mock.proposalExecutionPlan(this.proposal.id)).to.deep.equal([
|
||||
max(baseDelay, executionDelay),
|
||||
[true],
|
||||
[true],
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it('returns plan for not delayed operations', async function () {
|
||||
const roleId = 1n;
|
||||
const executionDelay = 0n;
|
||||
const baseDelay = 0n;
|
||||
|
||||
// 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);
|
||||
|
||||
this.proposal = await this.helper.setProposal([this.restricted.operation], `descr`);
|
||||
await this.helper.propose();
|
||||
|
||||
expect(await this.mock.proposalExecutionPlan(this.proposal.id)).to.deep.equal([0n, [true], [false]]);
|
||||
});
|
||||
|
||||
it('returns plan for an operation ignoring the manager', async function () {
|
||||
await this.mock.$_setAccessManagerIgnored(this.receiver, this.restricted.selector, true);
|
||||
|
||||
const roleId = 1n;
|
||||
const delays = [
|
||||
[time.duration.hours(1n), time.duration.hours(2n)],
|
||||
[time.duration.hours(2n), time.duration.hours(1n)],
|
||||
];
|
||||
|
||||
for (const [executionDelay, baseDelay] of delays) {
|
||||
// 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);
|
||||
|
||||
this.proposal = await this.helper.setProposal(
|
||||
[this.restricted.operation],
|
||||
`executionDelay=${executionDelay.toString()}}baseDelay=${baseDelay.toString()}}`,
|
||||
);
|
||||
await this.helper.propose();
|
||||
|
||||
expect(await this.mock.proposalExecutionPlan(this.proposal.id)).to.deep.equal([
|
||||
baseDelay,
|
||||
[false],
|
||||
[false],
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('base delay only', function () {
|
||||
for (const [delay, queue] of [
|
||||
[0, true],
|
||||
[0, false],
|
||||
[1000, true],
|
||||
]) {
|
||||
it(`delay ${delay}, ${queue ? 'with' : 'without'} queuing`, async function () {
|
||||
await this.mock.$_setBaseDelaySeconds(delay);
|
||||
|
||||
this.proposal = await this.helper.setProposal([this.unrestricted.operation], 'descr');
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
if (await this.mock.proposalNeedsQueuing(this.proposal.id)) {
|
||||
expect(await this.helper.queue())
|
||||
.to.emit(this.mock, 'ProposalQueued')
|
||||
.withArgs(this.proposal.id, anyValue);
|
||||
}
|
||||
if (delay > 0) {
|
||||
await this.helper.waitForEta();
|
||||
}
|
||||
await expect(this.helper.execute())
|
||||
.to.emit(this.mock, 'ProposalExecuted')
|
||||
.withArgs(this.proposal.id)
|
||||
.to.emit(this.receiver, 'CalledUnrestricted');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('reverts when an operation is executed before eta', async function () {
|
||||
const delay = time.duration.hours(2n);
|
||||
await this.mock.$_setBaseDelaySeconds(delay);
|
||||
|
||||
this.proposal = await this.helper.setProposal([this.unrestricted.operation], 'descr');
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await expect(this.helper.execute())
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorUnmetDelay')
|
||||
.withArgs(this.proposal.id, await this.mock.proposalEta(this.proposal.id));
|
||||
});
|
||||
|
||||
it('reverts with a proposal including multiple operations but one of those was cancelled in the manager', async function () {
|
||||
const delay = time.duration.hours(2n);
|
||||
const roleId = 1n;
|
||||
|
||||
await this.manager.connect(this.admin).setTargetFunctionRole(this.receiver, [this.restricted.selector], roleId);
|
||||
await this.manager.connect(this.admin).grantRole(roleId, this.mock, delay);
|
||||
|
||||
// Set proposals
|
||||
const original = new GovernorHelper(this.mock, mode);
|
||||
await original.setProposal([this.restricted.operation, this.unrestricted.operation], 'descr');
|
||||
|
||||
// Go through all the governance process
|
||||
await original.propose();
|
||||
await original.waitForSnapshot();
|
||||
await original.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await original.waitForDeadline();
|
||||
await original.queue();
|
||||
await original.waitForEta();
|
||||
|
||||
// Suddenly cancel one of the proposed operations in the manager
|
||||
await this.manager
|
||||
.connect(this.admin)
|
||||
.cancel(this.mock, this.restricted.operation.target, this.restricted.operation.data);
|
||||
|
||||
// Reschedule the same operation in a different proposal to avoid "AccessManagerNotScheduled" error
|
||||
const rescheduled = new GovernorHelper(this.mock, mode);
|
||||
await rescheduled.setProposal([this.restricted.operation], 'descr');
|
||||
await rescheduled.propose();
|
||||
await rescheduled.waitForSnapshot();
|
||||
await rescheduled.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await rescheduled.waitForDeadline();
|
||||
await rescheduled.queue(); // This will schedule it again in the manager
|
||||
await rescheduled.waitForEta();
|
||||
|
||||
// Attempt to execute
|
||||
await expect(original.execute())
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorMismatchedNonce')
|
||||
.withArgs(original.currentProposal.id, 1, 2);
|
||||
});
|
||||
|
||||
it('single operation with access manager delay', async function () {
|
||||
const delay = 1000n;
|
||||
const roleId = 1n;
|
||||
|
||||
await this.manager.connect(this.admin).setTargetFunctionRole(this.receiver, [this.restricted.selector], roleId);
|
||||
await this.manager.connect(this.admin).grantRole(roleId, this.mock, delay);
|
||||
|
||||
this.proposal = await this.helper.setProposal([this.restricted.operation], 'descr');
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
const txQueue = await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
const txExecute = await this.helper.execute();
|
||||
|
||||
await expect(txQueue)
|
||||
.to.emit(this.mock, 'ProposalQueued')
|
||||
.withArgs(this.proposal.id, anyValue)
|
||||
.to.emit(this.manager, 'OperationScheduled')
|
||||
.withArgs(
|
||||
this.restricted.id,
|
||||
1n,
|
||||
(await time.clockFromReceipt.timestamp(txQueue)) + delay,
|
||||
this.mock.target,
|
||||
this.restricted.operation.target,
|
||||
this.restricted.operation.data,
|
||||
);
|
||||
|
||||
await expect(txExecute)
|
||||
.to.emit(this.mock, 'ProposalExecuted')
|
||||
.withArgs(this.proposal.id)
|
||||
.to.emit(this.manager, 'OperationExecuted')
|
||||
.withArgs(this.restricted.id, 1n)
|
||||
.to.emit(this.receiver, 'CalledRestricted');
|
||||
});
|
||||
|
||||
it('bundle of varied operations', async function () {
|
||||
const managerDelay = 1000n;
|
||||
const roleId = 1n;
|
||||
const baseDelay = managerDelay * 2n;
|
||||
|
||||
await this.mock.$_setBaseDelaySeconds(baseDelay);
|
||||
|
||||
await this.manager.connect(this.admin).setTargetFunctionRole(this.receiver, [this.restricted.selector], roleId);
|
||||
await this.manager.connect(this.admin).grantRole(roleId, this.mock, managerDelay);
|
||||
|
||||
this.proposal = await this.helper.setProposal(
|
||||
[this.restricted.operation, this.unrestricted.operation, this.fallback.operation],
|
||||
'descr',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
const txQueue = await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
const txExecute = await this.helper.execute();
|
||||
|
||||
await expect(txQueue)
|
||||
.to.emit(this.mock, 'ProposalQueued')
|
||||
.withArgs(this.proposal.id, anyValue)
|
||||
.to.emit(this.manager, 'OperationScheduled')
|
||||
.withArgs(
|
||||
this.restricted.id,
|
||||
1n,
|
||||
(await time.clockFromReceipt.timestamp(txQueue)) + baseDelay,
|
||||
this.mock.target,
|
||||
this.restricted.operation.target,
|
||||
this.restricted.operation.data,
|
||||
);
|
||||
|
||||
await expect(txExecute)
|
||||
.to.emit(this.mock, 'ProposalExecuted')
|
||||
.withArgs(this.proposal.id)
|
||||
.to.emit(this.manager, 'OperationExecuted')
|
||||
.withArgs(this.restricted.id, 1n)
|
||||
.to.emit(this.receiver, 'CalledRestricted')
|
||||
.to.emit(this.receiver, 'CalledUnrestricted')
|
||||
.to.emit(this.receiver, 'CalledFallback');
|
||||
});
|
||||
|
||||
describe('cancel', function () {
|
||||
const delay = 1000n;
|
||||
const roleId = 1n;
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.manager
|
||||
.connect(this.admin)
|
||||
.setTargetFunctionRole(this.receiver, [this.restricted.selector], roleId);
|
||||
await this.manager.connect(this.admin).grantRole(roleId, this.mock, delay);
|
||||
});
|
||||
|
||||
it('cancels restricted with delay after queue (internal)', async function () {
|
||||
this.proposal = await this.helper.setProposal([this.restricted.operation], 'descr');
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
|
||||
await expect(this.helper.cancel('internal'))
|
||||
.to.emit(this.mock, 'ProposalCanceled')
|
||||
.withArgs(this.proposal.id)
|
||||
.to.emit(this.manager, 'OperationCanceled')
|
||||
.withArgs(this.restricted.id, 1n);
|
||||
|
||||
await this.helper.waitForEta();
|
||||
|
||||
await expect(this.helper.execute())
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
|
||||
.withArgs(
|
||||
this.proposal.id,
|
||||
ProposalState.Canceled,
|
||||
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
|
||||
);
|
||||
});
|
||||
|
||||
it('cancels restricted with queueing if the same operation is part of a more recent proposal (internal)', async function () {
|
||||
// Set proposals
|
||||
const original = new GovernorHelper(this.mock, mode);
|
||||
await original.setProposal([this.restricted.operation], 'descr');
|
||||
|
||||
// Go through all the governance process
|
||||
await original.propose();
|
||||
await original.waitForSnapshot();
|
||||
await original.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await original.waitForDeadline();
|
||||
await original.queue();
|
||||
|
||||
// Cancel the operation in the manager
|
||||
await this.manager
|
||||
.connect(this.admin)
|
||||
.cancel(this.mock, this.restricted.operation.target, this.restricted.operation.data);
|
||||
|
||||
// Another proposal is added with the same operation
|
||||
const rescheduled = new GovernorHelper(this.mock, mode);
|
||||
await rescheduled.setProposal([this.restricted.operation], 'another descr');
|
||||
|
||||
// Queue the new proposal
|
||||
await rescheduled.propose();
|
||||
await rescheduled.waitForSnapshot();
|
||||
await rescheduled.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await rescheduled.waitForDeadline();
|
||||
await rescheduled.queue(); // This will schedule it again in the manager
|
||||
|
||||
// Cancel
|
||||
const eta = await this.mock.proposalEta(rescheduled.currentProposal.id);
|
||||
|
||||
await expect(original.cancel('internal'))
|
||||
.to.emit(this.mock, 'ProposalCanceled')
|
||||
.withArgs(original.currentProposal.id);
|
||||
|
||||
await time.clock.timestamp().then(clock => time.increaseTo.timestamp(max(clock + 1n, eta)));
|
||||
|
||||
await expect(original.execute())
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
|
||||
.withArgs(
|
||||
original.currentProposal.id,
|
||||
ProposalState.Canceled,
|
||||
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
|
||||
);
|
||||
});
|
||||
|
||||
it('cancels unrestricted with queueing (internal)', async function () {
|
||||
this.proposal = await this.helper.setProposal([this.unrestricted.operation], 'descr');
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
|
||||
const eta = await this.mock.proposalEta(this.proposal.id);
|
||||
|
||||
await expect(this.helper.cancel('internal'))
|
||||
.to.emit(this.mock, 'ProposalCanceled')
|
||||
.withArgs(this.proposal.id);
|
||||
|
||||
await time.clock.timestamp().then(clock => time.increaseTo.timestamp(max(clock + 1n, eta)));
|
||||
|
||||
await expect(this.helper.execute())
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
|
||||
.withArgs(
|
||||
this.proposal.id,
|
||||
ProposalState.Canceled,
|
||||
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
|
||||
);
|
||||
});
|
||||
|
||||
it('cancels unrestricted without queueing (internal)', async function () {
|
||||
this.proposal = await this.helper.setProposal([this.unrestricted.operation], 'descr');
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
await expect(this.helper.cancel('internal'))
|
||||
.to.emit(this.mock, 'ProposalCanceled')
|
||||
.withArgs(this.proposal.id);
|
||||
|
||||
await expect(this.helper.execute())
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
|
||||
.withArgs(
|
||||
this.proposal.id,
|
||||
ProposalState.Canceled,
|
||||
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
|
||||
);
|
||||
});
|
||||
|
||||
it('cancels calls already canceled by guardian', async function () {
|
||||
const operationA = { target: this.receiver.target, data: this.restricted.selector + '00' };
|
||||
const operationB = { target: this.receiver.target, data: this.restricted.selector + '01' };
|
||||
const operationC = { target: this.receiver.target, data: this.restricted.selector + '02' };
|
||||
const operationAId = hashOperation(this.mock.target, operationA.target, operationA.data);
|
||||
const operationBId = hashOperation(this.mock.target, operationB.target, operationB.data);
|
||||
|
||||
const proposal1 = new GovernorHelper(this.mock, mode);
|
||||
const proposal2 = new GovernorHelper(this.mock, mode);
|
||||
proposal1.setProposal([operationA, operationB], 'proposal A+B');
|
||||
proposal2.setProposal([operationA, operationC], 'proposal A+C');
|
||||
|
||||
for (const p of [proposal1, proposal2]) {
|
||||
await p.propose();
|
||||
await p.waitForSnapshot();
|
||||
await p.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await p.waitForDeadline();
|
||||
}
|
||||
|
||||
// Can queue the first proposal
|
||||
await proposal1.queue();
|
||||
|
||||
// Cannot queue the second proposal: operation A already scheduled with delay
|
||||
await expect(proposal2.queue())
|
||||
.to.be.revertedWithCustomError(this.manager, 'AccessManagerAlreadyScheduled')
|
||||
.withArgs(operationAId);
|
||||
|
||||
// Admin cancels operation B on the manager
|
||||
await this.manager.connect(this.admin).cancel(this.mock, operationB.target, operationB.data);
|
||||
|
||||
// Still cannot queue the second proposal: operation A already scheduled with delay
|
||||
await expect(proposal2.queue())
|
||||
.to.be.revertedWithCustomError(this.manager, 'AccessManagerAlreadyScheduled')
|
||||
.withArgs(operationAId);
|
||||
|
||||
await proposal1.waitForEta();
|
||||
|
||||
// Cannot execute first proposal: operation B has been canceled
|
||||
await expect(proposal1.execute())
|
||||
.to.be.revertedWithCustomError(this.manager, 'AccessManagerNotScheduled')
|
||||
.withArgs(operationBId);
|
||||
|
||||
// Cancel the first proposal to release operation A
|
||||
await proposal1.cancel('internal');
|
||||
|
||||
// can finally queue the second proposal
|
||||
await proposal2.queue();
|
||||
|
||||
await proposal2.waitForEta();
|
||||
|
||||
// Can execute second proposal
|
||||
await proposal2.execute();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ignore AccessManager', function () {
|
||||
it('defaults', async function () {
|
||||
expect(await this.mock.isAccessManagerIgnored(this.receiver, this.restricted.selector)).to.be.false;
|
||||
expect(await this.mock.isAccessManagerIgnored(this.mock, '0x12341234')).to.be.true;
|
||||
});
|
||||
|
||||
it('internal setter', async function () {
|
||||
await expect(this.mock.$_setAccessManagerIgnored(this.receiver, this.restricted.selector, true))
|
||||
.to.emit(this.mock, 'AccessManagerIgnoredSet')
|
||||
.withArgs(this.receiver, this.restricted.selector, true);
|
||||
|
||||
expect(await this.mock.isAccessManagerIgnored(this.receiver, this.restricted.selector)).to.be.true;
|
||||
|
||||
await expect(this.mock.$_setAccessManagerIgnored(this.mock, '0x12341234', false))
|
||||
.to.emit(this.mock, 'AccessManagerIgnoredSet')
|
||||
.withArgs(this.mock, '0x12341234', false);
|
||||
|
||||
expect(await this.mock.isAccessManagerIgnored(this.mock, '0x12341234')).to.be.false;
|
||||
});
|
||||
|
||||
it('external setter', async function () {
|
||||
const setAccessManagerIgnored = (...args) =>
|
||||
this.mock.interface.encodeFunctionData('setAccessManagerIgnored', args);
|
||||
|
||||
await this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.target,
|
||||
data: setAccessManagerIgnored(
|
||||
this.receiver.target,
|
||||
[this.restricted.selector, this.unrestricted.selector],
|
||||
true,
|
||||
),
|
||||
},
|
||||
{
|
||||
target: this.mock.target,
|
||||
data: setAccessManagerIgnored(this.mock.target, ['0x12341234', '0x67896789'], false),
|
||||
},
|
||||
],
|
||||
'descr',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
await expect(this.helper.execute()).to.emit(this.mock, 'AccessManagerIgnoredSet');
|
||||
|
||||
expect(await this.mock.isAccessManagerIgnored(this.receiver, this.restricted.selector)).to.be.true;
|
||||
expect(await this.mock.isAccessManagerIgnored(this.receiver, this.unrestricted.selector)).to.be.true;
|
||||
expect(await this.mock.isAccessManagerIgnored(this.mock, '0x12341234')).to.be.false;
|
||||
expect(await this.mock.isAccessManagerIgnored(this.mock, '0x67896789')).to.be.false;
|
||||
});
|
||||
|
||||
it('locked function', async function () {
|
||||
const setAccessManagerIgnored = selector('setAccessManagerIgnored(address,bytes4[],bool)');
|
||||
|
||||
await expect(
|
||||
this.mock.$_setAccessManagerIgnored(this.mock, setAccessManagerIgnored, true),
|
||||
).to.be.revertedWithCustomError(this.mock, 'GovernorLockedIgnore');
|
||||
|
||||
await this.mock.$_setAccessManagerIgnored(this.receiver, setAccessManagerIgnored, true);
|
||||
});
|
||||
|
||||
it('ignores access manager', async function () {
|
||||
const amount = 100n;
|
||||
const target = this.token.target;
|
||||
const data = this.token.interface.encodeFunctionData('transfer', [this.voter4.address, amount]);
|
||||
const selector = data.slice(0, 10);
|
||||
await this.token.$_mint(this.mock, amount);
|
||||
|
||||
const roleId = 1n;
|
||||
await this.manager.connect(this.admin).setTargetFunctionRole(target, [selector], roleId);
|
||||
await this.manager.connect(this.admin).grantRole(roleId, this.mock, 0);
|
||||
|
||||
await this.helper.setProposal([{ target, data }], 'descr #1');
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
await expect(this.helper.execute())
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance')
|
||||
.withArgs(this.manager, 0n, amount);
|
||||
|
||||
await this.mock.$_setAccessManagerIgnored(target, selector, true);
|
||||
|
||||
await this.helper.setProposal([{ target, data }], 'descr #2');
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
await expect(this.helper.execute()).to.emit(this.token, 'Transfer').withArgs(this.mock, this.voter4, amount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('operating on an Ownable contract', function () {
|
||||
const method = selector('$_checkOwner()');
|
||||
|
||||
beforeEach(async function () {
|
||||
this.ownable = await ethers.deployContract('$Ownable', [this.manager]);
|
||||
this.operation = {
|
||||
target: this.ownable.target,
|
||||
data: this.ownable.interface.encodeFunctionData('$_checkOwner'),
|
||||
};
|
||||
});
|
||||
|
||||
it('succeeds with delay', async function () {
|
||||
const roleId = 1n;
|
||||
const executionDelay = time.duration.hours(2n);
|
||||
const baseDelay = time.duration.hours(1n);
|
||||
|
||||
// Set execution delay
|
||||
await this.manager.connect(this.admin).setTargetFunctionRole(this.ownable, [method], roleId);
|
||||
await this.manager.connect(this.admin).grantRole(roleId, this.mock, executionDelay);
|
||||
|
||||
// Set base delay
|
||||
await this.mock.$_setBaseDelaySeconds(baseDelay);
|
||||
|
||||
await this.helper.setProposal([this.operation], `descr`);
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
await this.helper.execute(); // Don't revert
|
||||
});
|
||||
|
||||
it('succeeds without delay', async function () {
|
||||
const roleId = 1n;
|
||||
const executionDelay = 0n;
|
||||
const baseDelay = 0n;
|
||||
|
||||
// Set execution delay
|
||||
await this.manager.connect(this.admin).setTargetFunctionRole(this.ownable, [method], roleId);
|
||||
await this.manager.connect(this.admin).grantRole(roleId, this.mock, executionDelay);
|
||||
|
||||
// Set base delay
|
||||
await this.mock.$_setBaseDelaySeconds(baseDelay);
|
||||
|
||||
await this.helper.setProposal([this.operation], `descr`);
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.execute(); // Don't revert
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,448 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs');
|
||||
|
||||
const { GovernorHelper } = require('../../helpers/governance');
|
||||
const { ProposalState, VoteType } = require('../../helpers/enums');
|
||||
const time = require('../../helpers/time');
|
||||
|
||||
const TOKENS = [
|
||||
{ Token: '$ERC20Votes', mode: 'blocknumber' },
|
||||
{ Token: '$ERC20VotesTimestampMock', mode: 'timestamp' },
|
||||
];
|
||||
|
||||
const name = 'OZ-Governor';
|
||||
const version = '1';
|
||||
const tokenName = 'MockToken';
|
||||
const tokenSymbol = 'MTKN';
|
||||
const tokenSupply = ethers.parseEther('100');
|
||||
const votingDelay = 4n;
|
||||
const votingPeriod = 16n;
|
||||
const value = ethers.parseEther('1');
|
||||
const defaultDelay = time.duration.days(2n);
|
||||
|
||||
describe('GovernorTimelockCompound', function () {
|
||||
for (const { Token, mode } of TOKENS) {
|
||||
const fixture = async () => {
|
||||
const [deployer, owner, voter1, voter2, voter3, voter4, other] = await ethers.getSigners();
|
||||
const receiver = await ethers.deployContract('CallReceiverMock');
|
||||
|
||||
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]);
|
||||
const predictGovernor = await deployer
|
||||
.getNonce()
|
||||
.then(nonce => ethers.getCreateAddress({ from: deployer.address, nonce: nonce + 1 }));
|
||||
const timelock = await ethers.deployContract('CompTimelock', [predictGovernor, defaultDelay]);
|
||||
const mock = await ethers.deployContract('$GovernorTimelockCompoundMock', [
|
||||
name,
|
||||
votingDelay,
|
||||
votingPeriod,
|
||||
0n,
|
||||
timelock,
|
||||
token,
|
||||
0n,
|
||||
]);
|
||||
|
||||
await owner.sendTransaction({ to: timelock, value });
|
||||
await token.$_mint(owner, tokenSupply);
|
||||
|
||||
const helper = new GovernorHelper(mock, mode);
|
||||
await helper.connect(owner).delegate({ token, to: voter1, value: ethers.parseEther('10') });
|
||||
await helper.connect(owner).delegate({ token, to: voter2, value: ethers.parseEther('7') });
|
||||
await helper.connect(owner).delegate({ token, to: voter3, value: ethers.parseEther('5') });
|
||||
await helper.connect(owner).delegate({ token, to: voter4, value: ethers.parseEther('2') });
|
||||
|
||||
return { deployer, owner, voter1, voter2, voter3, voter4, other, receiver, token, mock, timelock, helper };
|
||||
};
|
||||
|
||||
describe(`using ${Token}`, function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
|
||||
// default proposal
|
||||
this.proposal = this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.receiver.target,
|
||||
value,
|
||||
data: this.receiver.interface.encodeFunctionData('mockFunction'),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
});
|
||||
|
||||
it("doesn't accept ether transfers", async function () {
|
||||
await expect(this.owner.sendTransaction({ to: this.mock, value: 1n })).to.be.revertedWithCustomError(
|
||||
this.mock,
|
||||
'GovernorDisabledDeposit',
|
||||
);
|
||||
});
|
||||
|
||||
it('post deployment check', async function () {
|
||||
expect(await this.mock.name()).to.equal(name);
|
||||
expect(await this.mock.token()).to.equal(this.token);
|
||||
expect(await this.mock.votingDelay()).to.equal(votingDelay);
|
||||
expect(await this.mock.votingPeriod()).to.equal(votingPeriod);
|
||||
expect(await this.mock.quorum(0n)).to.equal(0n);
|
||||
|
||||
expect(await this.mock.timelock()).to.equal(this.timelock);
|
||||
expect(await this.timelock.admin()).to.equal(this.mock);
|
||||
});
|
||||
|
||||
it('nominal', async function () {
|
||||
expect(await this.mock.proposalEta(this.proposal.id)).to.equal(0n);
|
||||
expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.true;
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.connect(this.voter2).vote({ support: VoteType.For });
|
||||
await this.helper.connect(this.voter3).vote({ support: VoteType.Against });
|
||||
await this.helper.connect(this.voter4).vote({ support: VoteType.Abstain });
|
||||
await this.helper.waitForDeadline();
|
||||
const txQueue = await this.helper.queue();
|
||||
|
||||
const eta = (await time.clockFromReceipt.timestamp(txQueue)) + defaultDelay;
|
||||
expect(await this.mock.proposalEta(this.proposal.id)).to.equal(eta);
|
||||
expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.true;
|
||||
|
||||
await this.helper.waitForEta();
|
||||
const txExecute = await this.helper.execute();
|
||||
|
||||
await expect(txQueue)
|
||||
.to.emit(this.mock, 'ProposalQueued')
|
||||
.withArgs(this.proposal.id, eta)
|
||||
.to.emit(this.timelock, 'QueueTransaction')
|
||||
.withArgs(...Array(5).fill(anyValue), eta);
|
||||
|
||||
await expect(txExecute)
|
||||
.to.emit(this.mock, 'ProposalExecuted')
|
||||
.withArgs(this.proposal.id)
|
||||
.to.emit(this.timelock, 'ExecuteTransaction')
|
||||
.withArgs(...Array(5).fill(anyValue), eta)
|
||||
.to.emit(this.receiver, 'MockFunctionCalled');
|
||||
});
|
||||
|
||||
describe('should revert', function () {
|
||||
describe('on queue', function () {
|
||||
it('if already queued', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await expect(this.helper.queue())
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
|
||||
.withArgs(
|
||||
this.proposal.id,
|
||||
ProposalState.Queued,
|
||||
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded]),
|
||||
);
|
||||
});
|
||||
|
||||
it('if proposal contains duplicate calls', async function () {
|
||||
const action = {
|
||||
target: this.token.target,
|
||||
data: this.token.interface.encodeFunctionData('approve', [this.receiver.target, ethers.MaxUint256]),
|
||||
};
|
||||
const { id } = this.helper.setProposal([action, action], '<proposal description>');
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await expect(this.helper.queue())
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorAlreadyQueuedProposal')
|
||||
.withArgs(id);
|
||||
await expect(this.helper.execute())
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorNotQueuedProposal')
|
||||
.withArgs(id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on execute', function () {
|
||||
it('if not queued', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
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);
|
||||
});
|
||||
|
||||
it('if too early', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
|
||||
expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Queued);
|
||||
|
||||
await expect(this.helper.execute()).to.be.rejectedWith(
|
||||
"Timelock::executeTransaction: Transaction hasn't surpassed time lock",
|
||||
);
|
||||
});
|
||||
|
||||
it('if too late', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta(time.duration.days(30));
|
||||
|
||||
expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Expired);
|
||||
|
||||
await expect(this.helper.execute())
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
|
||||
.withArgs(
|
||||
this.proposal.id,
|
||||
ProposalState.Expired,
|
||||
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
|
||||
);
|
||||
});
|
||||
|
||||
it('if already executed', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
await this.helper.execute();
|
||||
|
||||
await expect(this.helper.execute())
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
|
||||
.withArgs(
|
||||
this.proposal.id,
|
||||
ProposalState.Executed,
|
||||
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on safe receive', function () {
|
||||
describe('ERC721', function () {
|
||||
const tokenId = 1n;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ethers.deployContract('$ERC721', ['Non Fungible Token', 'NFT']);
|
||||
await this.token.$_mint(this.owner, tokenId);
|
||||
});
|
||||
|
||||
it("can't receive an ERC721 safeTransfer", async function () {
|
||||
await expect(
|
||||
this.token.connect(this.owner).safeTransferFrom(this.owner, this.mock, tokenId),
|
||||
).to.be.revertedWithCustomError(this.mock, 'GovernorDisabledDeposit');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ERC1155', function () {
|
||||
const tokenIds = {
|
||||
1: 1000n,
|
||||
2: 2000n,
|
||||
3: 3000n,
|
||||
};
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ethers.deployContract('$ERC1155', ['https://token-cdn-domain/{id}.json']);
|
||||
await this.token.$_mintBatch(this.owner, Object.keys(tokenIds), Object.values(tokenIds), '0x');
|
||||
});
|
||||
|
||||
it("can't receive ERC1155 safeTransfer", async function () {
|
||||
await expect(
|
||||
this.token.connect(this.owner).safeTransferFrom(
|
||||
this.owner,
|
||||
this.mock,
|
||||
...Object.entries(tokenIds)[0], // id + amount
|
||||
'0x',
|
||||
),
|
||||
).to.be.revertedWithCustomError(this.mock, 'GovernorDisabledDeposit');
|
||||
});
|
||||
|
||||
it("can't receive ERC1155 safeBatchTransfer", async function () {
|
||||
await expect(
|
||||
this.token
|
||||
.connect(this.owner)
|
||||
.safeBatchTransferFrom(this.owner, this.mock, Object.keys(tokenIds), Object.values(tokenIds), '0x'),
|
||||
).to.be.revertedWithCustomError(this.mock, 'GovernorDisabledDeposit');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancel', function () {
|
||||
it('cancel before queue prevents scheduling', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
await expect(this.helper.cancel('internal'))
|
||||
.to.emit(this.mock, 'ProposalCanceled')
|
||||
.withArgs(this.proposal.id);
|
||||
|
||||
expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Canceled);
|
||||
|
||||
await expect(this.helper.queue())
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
|
||||
.withArgs(
|
||||
this.proposal.id,
|
||||
ProposalState.Canceled,
|
||||
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded]),
|
||||
);
|
||||
});
|
||||
|
||||
it('cancel after queue prevents executing', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
|
||||
await expect(this.helper.cancel('internal'))
|
||||
.to.emit(this.mock, 'ProposalCanceled')
|
||||
.withArgs(this.proposal.id);
|
||||
|
||||
expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Canceled);
|
||||
|
||||
await expect(this.helper.execute())
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
|
||||
.withArgs(
|
||||
this.proposal.id,
|
||||
ProposalState.Canceled,
|
||||
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onlyGovernance', function () {
|
||||
describe('relay', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(this.mock, 1);
|
||||
});
|
||||
|
||||
it('is protected', async function () {
|
||||
await expect(
|
||||
this.mock
|
||||
.connect(this.owner)
|
||||
.relay(this.token, 0, this.token.interface.encodeFunctionData('transfer', [this.other.address, 1n])),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor')
|
||||
.withArgs(this.owner);
|
||||
});
|
||||
|
||||
it('can be executed through governance', async function () {
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.target,
|
||||
data: this.mock.interface.encodeFunctionData('relay', [
|
||||
this.token.target,
|
||||
0n,
|
||||
this.token.interface.encodeFunctionData('transfer', [this.other.address, 1n]),
|
||||
]),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
|
||||
const txExecute = this.helper.execute();
|
||||
|
||||
await expect(txExecute).to.changeTokenBalances(this.token, [this.mock, this.other], [-1n, 1n]);
|
||||
|
||||
await expect(txExecute).to.emit(this.token, 'Transfer').withArgs(this.mock, this.other, 1n);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTimelock', function () {
|
||||
beforeEach(async function () {
|
||||
this.newTimelock = await ethers.deployContract('CompTimelock', [this.mock, time.duration.days(7n)]);
|
||||
});
|
||||
|
||||
it('is protected', async function () {
|
||||
await expect(this.mock.connect(this.owner).updateTimelock(this.newTimelock))
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor')
|
||||
.withArgs(this.owner);
|
||||
});
|
||||
|
||||
it('can be executed through governance to', async function () {
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.timelock.target,
|
||||
data: this.timelock.interface.encodeFunctionData('setPendingAdmin', [this.owner.address]),
|
||||
},
|
||||
{
|
||||
target: this.mock.target,
|
||||
data: this.mock.interface.encodeFunctionData('updateTimelock', [this.newTimelock.target]),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
|
||||
await expect(this.helper.execute())
|
||||
.to.emit(this.mock, 'TimelockChange')
|
||||
.withArgs(this.timelock, this.newTimelock);
|
||||
|
||||
expect(await this.mock.timelock()).to.equal(this.newTimelock);
|
||||
});
|
||||
});
|
||||
|
||||
it('can transfer timelock to new governor', async function () {
|
||||
const newGovernor = await ethers.deployContract('$GovernorTimelockCompoundMock', [
|
||||
name,
|
||||
8n,
|
||||
32n,
|
||||
0n,
|
||||
this.timelock,
|
||||
this.token,
|
||||
0n,
|
||||
]);
|
||||
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.timelock.target,
|
||||
data: this.timelock.interface.encodeFunctionData('setPendingAdmin', [newGovernor.target]),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
|
||||
await expect(this.helper.execute()).to.emit(this.timelock, 'NewPendingAdmin').withArgs(newGovernor);
|
||||
|
||||
await newGovernor.__acceptAdmin();
|
||||
expect(await this.timelock.admin()).to.equal(newGovernor);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,504 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs');
|
||||
const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');
|
||||
|
||||
const { GovernorHelper, timelockSalt } = require('../../helpers/governance');
|
||||
const { OperationState, ProposalState, VoteType } = require('../../helpers/enums');
|
||||
const time = require('../../helpers/time');
|
||||
|
||||
const TOKENS = [
|
||||
{ Token: '$ERC20Votes', mode: 'blocknumber' },
|
||||
{ Token: '$ERC20VotesTimestampMock', mode: 'timestamp' },
|
||||
];
|
||||
|
||||
const DEFAULT_ADMIN_ROLE = ethers.ZeroHash;
|
||||
const PROPOSER_ROLE = ethers.id('PROPOSER_ROLE');
|
||||
const EXECUTOR_ROLE = ethers.id('EXECUTOR_ROLE');
|
||||
const CANCELLER_ROLE = ethers.id('CANCELLER_ROLE');
|
||||
|
||||
const name = 'OZ-Governor';
|
||||
const version = '1';
|
||||
const tokenName = 'MockToken';
|
||||
const tokenSymbol = 'MTKN';
|
||||
const tokenSupply = ethers.parseEther('100');
|
||||
const votingDelay = 4n;
|
||||
const votingPeriod = 16n;
|
||||
const value = ethers.parseEther('1');
|
||||
const delay = time.duration.hours(1n);
|
||||
|
||||
describe('GovernorTimelockControl', function () {
|
||||
for (const { Token, mode } of TOKENS) {
|
||||
const fixture = async () => {
|
||||
const [deployer, owner, voter1, voter2, voter3, voter4, other] = await ethers.getSigners();
|
||||
const receiver = await ethers.deployContract('CallReceiverMock');
|
||||
|
||||
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]);
|
||||
const timelock = await ethers.deployContract('TimelockController', [delay, [], [], deployer]);
|
||||
const mock = await ethers.deployContract('$GovernorTimelockControlMock', [
|
||||
name,
|
||||
votingDelay,
|
||||
votingPeriod,
|
||||
0n,
|
||||
timelock,
|
||||
token,
|
||||
0n,
|
||||
]);
|
||||
|
||||
await owner.sendTransaction({ to: timelock, value });
|
||||
await token.$_mint(owner, tokenSupply);
|
||||
await timelock.grantRole(PROPOSER_ROLE, mock);
|
||||
await timelock.grantRole(PROPOSER_ROLE, owner);
|
||||
await timelock.grantRole(CANCELLER_ROLE, mock);
|
||||
await timelock.grantRole(CANCELLER_ROLE, owner);
|
||||
await timelock.grantRole(EXECUTOR_ROLE, ethers.ZeroAddress);
|
||||
await timelock.revokeRole(DEFAULT_ADMIN_ROLE, deployer);
|
||||
|
||||
const helper = new GovernorHelper(mock, mode);
|
||||
await helper.connect(owner).delegate({ token, to: voter1, value: ethers.parseEther('10') });
|
||||
await helper.connect(owner).delegate({ token, to: voter2, value: ethers.parseEther('7') });
|
||||
await helper.connect(owner).delegate({ token, to: voter3, value: ethers.parseEther('5') });
|
||||
await helper.connect(owner).delegate({ token, to: voter4, value: ethers.parseEther('2') });
|
||||
|
||||
return { deployer, owner, voter1, voter2, voter3, voter4, other, receiver, token, mock, timelock, helper };
|
||||
};
|
||||
|
||||
describe(`using ${Token}`, function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
|
||||
// default proposal
|
||||
this.proposal = this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.receiver.target,
|
||||
value,
|
||||
data: this.receiver.interface.encodeFunctionData('mockFunction'),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
this.proposal.timelockid = await this.timelock.hashOperationBatch(
|
||||
...this.proposal.shortProposal.slice(0, 3),
|
||||
ethers.ZeroHash,
|
||||
timelockSalt(this.mock.target, this.proposal.shortProposal[3]),
|
||||
);
|
||||
});
|
||||
|
||||
it("doesn't accept ether transfers", async function () {
|
||||
await expect(this.owner.sendTransaction({ to: this.mock, value: 1n })).to.be.revertedWithCustomError(
|
||||
this.mock,
|
||||
'GovernorDisabledDeposit',
|
||||
);
|
||||
});
|
||||
|
||||
it('post deployment check', async function () {
|
||||
expect(await this.mock.name()).to.equal(name);
|
||||
expect(await this.mock.token()).to.equal(this.token);
|
||||
expect(await this.mock.votingDelay()).to.equal(votingDelay);
|
||||
expect(await this.mock.votingPeriod()).to.equal(votingPeriod);
|
||||
expect(await this.mock.quorum(0n)).to.equal(0n);
|
||||
|
||||
expect(await this.mock.timelock()).to.equal(this.timelock);
|
||||
});
|
||||
|
||||
it('nominal', async function () {
|
||||
expect(await this.mock.proposalEta(this.proposal.id)).to.equal(0n);
|
||||
expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.true;
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.connect(this.voter2).vote({ support: VoteType.For });
|
||||
await this.helper.connect(this.voter3).vote({ support: VoteType.Against });
|
||||
await this.helper.connect(this.voter4).vote({ support: VoteType.Abstain });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.true;
|
||||
const txQueue = await this.helper.queue();
|
||||
|
||||
const eta = (await time.clockFromReceipt.timestamp(txQueue)) + delay;
|
||||
expect(await this.mock.proposalEta(this.proposal.id)).to.equal(eta);
|
||||
await this.helper.waitForEta();
|
||||
|
||||
const txExecute = this.helper.execute();
|
||||
|
||||
await expect(txQueue)
|
||||
.to.emit(this.mock, 'ProposalQueued')
|
||||
.withArgs(this.proposal.id, anyValue)
|
||||
.to.emit(this.timelock, 'CallScheduled')
|
||||
.withArgs(this.proposal.timelockid, ...Array(6).fill(anyValue))
|
||||
.to.emit(this.timelock, 'CallSalt')
|
||||
.withArgs(this.proposal.timelockid, anyValue);
|
||||
|
||||
await expect(txExecute)
|
||||
.to.emit(this.mock, 'ProposalExecuted')
|
||||
.withArgs(this.proposal.id)
|
||||
.to.emit(this.timelock, 'CallExecuted')
|
||||
.withArgs(this.proposal.timelockid, ...Array(4).fill(anyValue))
|
||||
.to.emit(this.receiver, 'MockFunctionCalled');
|
||||
});
|
||||
|
||||
describe('should revert', function () {
|
||||
describe('on queue', function () {
|
||||
it('if already queued', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await expect(this.helper.queue())
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
|
||||
.withArgs(
|
||||
this.proposal.id,
|
||||
ProposalState.Queued,
|
||||
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on execute', function () {
|
||||
it('if not queued', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
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));
|
||||
});
|
||||
|
||||
it('if too early', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
|
||||
expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Queued);
|
||||
|
||||
await expect(this.helper.execute())
|
||||
.to.be.revertedWithCustomError(this.timelock, 'TimelockUnexpectedOperationState')
|
||||
.withArgs(this.proposal.timelockid, GovernorHelper.proposalStatesToBitMap(OperationState.Ready));
|
||||
});
|
||||
|
||||
it('if already executed', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
await this.helper.execute();
|
||||
|
||||
await expect(this.helper.execute())
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
|
||||
.withArgs(
|
||||
this.proposal.id,
|
||||
ProposalState.Executed,
|
||||
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
|
||||
);
|
||||
});
|
||||
|
||||
it('if already executed by another proposer', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
|
||||
await this.timelock.executeBatch(
|
||||
...this.proposal.shortProposal.slice(0, 3),
|
||||
ethers.ZeroHash,
|
||||
timelockSalt(this.mock.target, this.proposal.shortProposal[3]),
|
||||
);
|
||||
|
||||
await expect(this.helper.execute())
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
|
||||
.withArgs(
|
||||
this.proposal.id,
|
||||
ProposalState.Executed,
|
||||
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancel', function () {
|
||||
it('cancel before queue prevents scheduling', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
await expect(this.helper.cancel('internal'))
|
||||
.to.emit(this.mock, 'ProposalCanceled')
|
||||
.withArgs(this.proposal.id);
|
||||
|
||||
expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Canceled);
|
||||
|
||||
await expect(this.helper.queue())
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
|
||||
.withArgs(
|
||||
this.proposal.id,
|
||||
ProposalState.Canceled,
|
||||
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded]),
|
||||
);
|
||||
});
|
||||
|
||||
it('cancel after queue prevents executing', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
|
||||
await expect(this.helper.cancel('internal'))
|
||||
.to.emit(this.mock, 'ProposalCanceled')
|
||||
.withArgs(this.proposal.id);
|
||||
|
||||
expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Canceled);
|
||||
|
||||
await expect(this.helper.execute())
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
|
||||
.withArgs(
|
||||
this.proposal.id,
|
||||
ProposalState.Canceled,
|
||||
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
|
||||
);
|
||||
});
|
||||
|
||||
it('cancel on timelock is reflected on governor', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
|
||||
expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Queued);
|
||||
|
||||
await expect(this.timelock.connect(this.owner).cancel(this.proposal.timelockid))
|
||||
.to.emit(this.timelock, 'Cancelled')
|
||||
.withArgs(this.proposal.timelockid);
|
||||
|
||||
expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Canceled);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onlyGovernance', function () {
|
||||
describe('relay', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(this.mock, 1);
|
||||
});
|
||||
|
||||
it('is protected', async function () {
|
||||
await expect(
|
||||
this.mock
|
||||
.connect(this.owner)
|
||||
.relay(this.token, 0n, this.token.interface.encodeFunctionData('transfer', [this.other.address, 1n])),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor')
|
||||
.withArgs(this.owner);
|
||||
});
|
||||
|
||||
it('can be executed through governance', async function () {
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.target,
|
||||
data: this.mock.interface.encodeFunctionData('relay', [
|
||||
this.token.target,
|
||||
0n,
|
||||
this.token.interface.encodeFunctionData('transfer', [this.other.address, 1n]),
|
||||
]),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
|
||||
const txExecute = await this.helper.execute();
|
||||
|
||||
await expect(txExecute).to.changeTokenBalances(this.token, [this.mock, this.other], [-1n, 1n]);
|
||||
|
||||
await expect(txExecute).to.emit(this.token, 'Transfer').withArgs(this.mock, this.other, 1n);
|
||||
});
|
||||
|
||||
it('is payable and can transfer eth to EOA', async function () {
|
||||
const t2g = 128n; // timelock to governor
|
||||
const g2o = 100n; // governor to eoa (other)
|
||||
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.target,
|
||||
value: t2g,
|
||||
data: this.mock.interface.encodeFunctionData('relay', [this.other.address, g2o, '0x']),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
|
||||
await expect(this.helper.execute()).to.changeEtherBalances(
|
||||
[this.timelock, this.mock, this.other],
|
||||
[-t2g, t2g - g2o, g2o],
|
||||
);
|
||||
});
|
||||
|
||||
it('protected against other proposers', async function () {
|
||||
const call = [
|
||||
this.mock,
|
||||
0n,
|
||||
this.mock.interface.encodeFunctionData('relay', [ethers.ZeroAddress, 0n, '0x']),
|
||||
ethers.ZeroHash,
|
||||
ethers.ZeroHash,
|
||||
];
|
||||
|
||||
await this.timelock.connect(this.owner).schedule(...call, delay);
|
||||
|
||||
await time.increaseBy.timestamp(delay);
|
||||
|
||||
// Error bubbled up from Governor
|
||||
await expect(this.timelock.connect(this.owner).execute(...call)).to.be.revertedWithPanic(
|
||||
PANIC_CODES.POP_ON_EMPTY_ARRAY,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTimelock', function () {
|
||||
beforeEach(async function () {
|
||||
this.newTimelock = await ethers.deployContract('TimelockController', [
|
||||
delay,
|
||||
[this.mock],
|
||||
[this.mock],
|
||||
ethers.ZeroAddress,
|
||||
]);
|
||||
});
|
||||
|
||||
it('is protected', async function () {
|
||||
await expect(this.mock.connect(this.owner).updateTimelock(this.newTimelock))
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor')
|
||||
.withArgs(this.owner);
|
||||
});
|
||||
|
||||
it('can be executed through governance to', async function () {
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.target,
|
||||
data: this.mock.interface.encodeFunctionData('updateTimelock', [this.newTimelock.target]),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
|
||||
await expect(this.helper.execute())
|
||||
.to.emit(this.mock, 'TimelockChange')
|
||||
.withArgs(this.timelock, this.newTimelock);
|
||||
|
||||
expect(await this.mock.timelock()).to.equal(this.newTimelock);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on safe receive', function () {
|
||||
describe('ERC721', function () {
|
||||
const tokenId = 1n;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ethers.deployContract('$ERC721', ['Non Fungible Token', 'NFT']);
|
||||
await this.token.$_mint(this.owner, tokenId);
|
||||
});
|
||||
|
||||
it("can't receive an ERC721 safeTransfer", async function () {
|
||||
await expect(
|
||||
this.token.connect(this.owner).safeTransferFrom(this.owner, this.mock, tokenId),
|
||||
).to.be.revertedWithCustomError(this.mock, 'GovernorDisabledDeposit');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ERC1155', function () {
|
||||
const tokenIds = {
|
||||
1: 1000n,
|
||||
2: 2000n,
|
||||
3: 3000n,
|
||||
};
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ethers.deployContract('$ERC1155', ['https://token-cdn-domain/{id}.json']);
|
||||
await this.token.$_mintBatch(this.owner, Object.keys(tokenIds), Object.values(tokenIds), '0x');
|
||||
});
|
||||
|
||||
it("can't receive ERC1155 safeTransfer", async function () {
|
||||
await expect(
|
||||
this.token.connect(this.owner).safeTransferFrom(
|
||||
this.owner,
|
||||
this.mock,
|
||||
...Object.entries(tokenIds)[0], // id + amount
|
||||
'0x',
|
||||
),
|
||||
).to.be.revertedWithCustomError(this.mock, 'GovernorDisabledDeposit');
|
||||
});
|
||||
|
||||
it("can't receive ERC1155 safeBatchTransfer", async function () {
|
||||
await expect(
|
||||
this.token
|
||||
.connect(this.owner)
|
||||
.safeBatchTransferFrom(this.owner, this.mock, Object.keys(tokenIds), Object.values(tokenIds), '0x'),
|
||||
).to.be.revertedWithCustomError(this.mock, 'GovernorDisabledDeposit');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('clear queue of pending governor calls', async function () {
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.target,
|
||||
data: this.mock.interface.encodeFunctionData('nonGovernanceFunction'),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
await this.helper.execute();
|
||||
|
||||
// This path clears _governanceCall as part of the afterExecute call,
|
||||
// but we have not way to check that the cleanup actually happened other
|
||||
// then coverage reports.
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture, mine } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { GovernorHelper } = require('../../helpers/governance');
|
||||
const { ProposalState, VoteType } = require('../../helpers/enums');
|
||||
const time = require('../../helpers/time');
|
||||
|
||||
const TOKENS = [
|
||||
{ Token: '$ERC20Votes', mode: 'blocknumber' },
|
||||
{ Token: '$ERC20VotesTimestampMock', mode: 'timestamp' },
|
||||
];
|
||||
|
||||
const name = 'OZ-Governor';
|
||||
const version = '1';
|
||||
const tokenName = 'MockToken';
|
||||
const tokenSymbol = 'MTKN';
|
||||
const tokenSupply = ethers.parseEther('100');
|
||||
const ratio = 8n; // percents
|
||||
const newRatio = 6n; // percents
|
||||
const votingDelay = 4n;
|
||||
const votingPeriod = 16n;
|
||||
const value = ethers.parseEther('1');
|
||||
|
||||
describe('GovernorVotesQuorumFraction', function () {
|
||||
for (const { Token, mode } of TOKENS) {
|
||||
const fixture = async () => {
|
||||
const [owner, voter1, voter2, voter3, voter4] = await ethers.getSigners();
|
||||
|
||||
const receiver = await ethers.deployContract('CallReceiverMock');
|
||||
|
||||
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]);
|
||||
const mock = await ethers.deployContract('$GovernorMock', [name, votingDelay, votingPeriod, 0n, token, ratio]);
|
||||
|
||||
await owner.sendTransaction({ to: mock, value });
|
||||
await token.$_mint(owner, tokenSupply);
|
||||
|
||||
const helper = new GovernorHelper(mock, mode);
|
||||
await helper.connect(owner).delegate({ token, to: voter1, value: ethers.parseEther('10') });
|
||||
await helper.connect(owner).delegate({ token, to: voter2, value: ethers.parseEther('7') });
|
||||
await helper.connect(owner).delegate({ token, to: voter3, value: ethers.parseEther('5') });
|
||||
await helper.connect(owner).delegate({ token, to: voter4, value: ethers.parseEther('2') });
|
||||
|
||||
return { owner, voter1, voter2, voter3, voter4, receiver, token, mock, helper };
|
||||
};
|
||||
|
||||
describe(`using ${Token}`, function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
|
||||
// default proposal
|
||||
this.proposal = this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.receiver.target,
|
||||
value,
|
||||
data: this.receiver.interface.encodeFunctionData('mockFunction'),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
});
|
||||
|
||||
it('deployment check', async function () {
|
||||
expect(await this.mock.name()).to.equal(name);
|
||||
expect(await this.mock.token()).to.equal(this.token);
|
||||
expect(await this.mock.votingDelay()).to.equal(votingDelay);
|
||||
expect(await this.mock.votingPeriod()).to.equal(votingPeriod);
|
||||
expect(await this.mock.quorum(0)).to.equal(0n);
|
||||
expect(await this.mock.quorumNumerator()).to.equal(ratio);
|
||||
expect(await this.mock.quorumDenominator()).to.equal(100n);
|
||||
expect(await time.clock[mode]().then(clock => this.mock.quorum(clock - 1n))).to.equal(
|
||||
(tokenSupply * ratio) / 100n,
|
||||
);
|
||||
});
|
||||
|
||||
it('quroum reached', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.execute();
|
||||
});
|
||||
|
||||
it('quroum not reached', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter2).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
await expect(this.helper.execute())
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
|
||||
.withArgs(
|
||||
this.proposal.id,
|
||||
ProposalState.Defeated,
|
||||
GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
|
||||
);
|
||||
});
|
||||
|
||||
describe('onlyGovernance updates', function () {
|
||||
it('updateQuorumNumerator is protected', async function () {
|
||||
await expect(this.mock.connect(this.owner).updateQuorumNumerator(newRatio))
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor')
|
||||
.withArgs(this.owner);
|
||||
});
|
||||
|
||||
it('can updateQuorumNumerator through governance', async function () {
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.target,
|
||||
data: this.mock.interface.encodeFunctionData('updateQuorumNumerator', [newRatio]),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
await expect(this.helper.execute()).to.emit(this.mock, 'QuorumNumeratorUpdated').withArgs(ratio, newRatio);
|
||||
|
||||
expect(await this.mock.quorumNumerator()).to.equal(newRatio);
|
||||
expect(await this.mock.quorumDenominator()).to.equal(100n);
|
||||
|
||||
// it takes one block for the new quorum to take effect
|
||||
expect(await time.clock[mode]().then(blockNumber => this.mock.quorum(blockNumber - 1n))).to.equal(
|
||||
(tokenSupply * ratio) / 100n,
|
||||
);
|
||||
|
||||
await mine();
|
||||
|
||||
expect(await time.clock[mode]().then(blockNumber => this.mock.quorum(blockNumber - 1n))).to.equal(
|
||||
(tokenSupply * newRatio) / 100n,
|
||||
);
|
||||
});
|
||||
|
||||
it('cannot updateQuorumNumerator over the maximum', async function () {
|
||||
const quorumNumerator = 101n;
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.mock.target,
|
||||
data: this.mock.interface.encodeFunctionData('updateQuorumNumerator', [quorumNumerator]),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
|
||||
await this.helper.waitForDeadline();
|
||||
|
||||
const quorumDenominator = await this.mock.quorumDenominator();
|
||||
|
||||
await expect(this.helper.execute())
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorInvalidQuorumFraction')
|
||||
.withArgs(quorumNumerator, quorumDenominator);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,245 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { GovernorHelper } = require('../../helpers/governance');
|
||||
const { VoteType } = require('../../helpers/enums');
|
||||
const { getDomain, ExtendedBallot } = require('../../helpers/eip712');
|
||||
|
||||
const TOKENS = [
|
||||
{ Token: '$ERC20Votes', mode: 'blocknumber' },
|
||||
{ Token: '$ERC20VotesTimestampMock', mode: 'timestamp' },
|
||||
];
|
||||
|
||||
const name = 'OZ-Governor';
|
||||
const version = '1';
|
||||
const tokenName = 'MockToken';
|
||||
const tokenSymbol = 'MTKN';
|
||||
const tokenSupply = ethers.parseEther('100');
|
||||
const votingDelay = 4n;
|
||||
const votingPeriod = 16n;
|
||||
const value = ethers.parseEther('1');
|
||||
|
||||
const params = {
|
||||
decoded: [42n, 'These are my params'],
|
||||
encoded: ethers.AbiCoder.defaultAbiCoder().encode(['uint256', 'string'], [42n, 'These are my params']),
|
||||
};
|
||||
|
||||
describe('GovernorWithParams', function () {
|
||||
for (const { Token, mode } of TOKENS) {
|
||||
const fixture = async () => {
|
||||
const [owner, proposer, voter1, voter2, voter3, voter4, other] = await ethers.getSigners();
|
||||
const receiver = await ethers.deployContract('CallReceiverMock');
|
||||
|
||||
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]);
|
||||
const mock = await ethers.deployContract('$GovernorWithParamsMock', [name, token]);
|
||||
|
||||
await owner.sendTransaction({ to: mock, value });
|
||||
await token.$_mint(owner, tokenSupply);
|
||||
|
||||
const helper = new GovernorHelper(mock, mode);
|
||||
await helper.connect(owner).delegate({ token, to: voter1, value: ethers.parseEther('10') });
|
||||
await helper.connect(owner).delegate({ token, to: voter2, value: ethers.parseEther('7') });
|
||||
await helper.connect(owner).delegate({ token, to: voter3, value: ethers.parseEther('5') });
|
||||
await helper.connect(owner).delegate({ token, to: voter4, value: ethers.parseEther('2') });
|
||||
|
||||
return { owner, proposer, voter1, voter2, voter3, voter4, other, receiver, token, mock, helper };
|
||||
};
|
||||
|
||||
describe(`using ${Token}`, function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
|
||||
// default proposal
|
||||
this.proposal = this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.receiver.target,
|
||||
value,
|
||||
data: this.receiver.interface.encodeFunctionData('mockFunction'),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
});
|
||||
|
||||
it('deployment check', async function () {
|
||||
expect(await this.mock.name()).to.equal(name);
|
||||
expect(await this.mock.token()).to.equal(this.token);
|
||||
expect(await this.mock.votingDelay()).to.equal(votingDelay);
|
||||
expect(await this.mock.votingPeriod()).to.equal(votingPeriod);
|
||||
});
|
||||
|
||||
it('nominal is unaffected', async function () {
|
||||
await this.helper.connect(this.proposer).propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.connect(this.voter1).vote({ support: VoteType.For, reason: 'This is nice' });
|
||||
await this.helper.connect(this.voter2).vote({ support: VoteType.For });
|
||||
await this.helper.connect(this.voter3).vote({ support: VoteType.Against });
|
||||
await this.helper.connect(this.voter4).vote({ support: VoteType.Abstain });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.execute();
|
||||
|
||||
expect(await this.mock.hasVoted(this.proposal.id, this.owner)).to.be.false;
|
||||
expect(await this.mock.hasVoted(this.proposal.id, this.voter1)).to.be.true;
|
||||
expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.be.true;
|
||||
expect(await ethers.provider.getBalance(this.mock)).to.equal(0n);
|
||||
expect(await ethers.provider.getBalance(this.receiver)).to.equal(value);
|
||||
});
|
||||
|
||||
it('Voting with params is properly supported', async function () {
|
||||
await this.helper.connect(this.proposer).propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
|
||||
const weight = ethers.parseEther('7') - params.decoded[0];
|
||||
|
||||
await expect(
|
||||
this.helper.connect(this.voter2).vote({
|
||||
support: VoteType.For,
|
||||
reason: 'no particular reason',
|
||||
params: params.encoded,
|
||||
}),
|
||||
)
|
||||
.to.emit(this.mock, 'CountParams')
|
||||
.withArgs(...params.decoded)
|
||||
.to.emit(this.mock, 'VoteCastWithParams')
|
||||
.withArgs(
|
||||
this.voter2.address,
|
||||
this.proposal.id,
|
||||
VoteType.For,
|
||||
weight,
|
||||
'no particular reason',
|
||||
params.encoded,
|
||||
);
|
||||
|
||||
expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([0n, weight, 0n]);
|
||||
});
|
||||
|
||||
describe('voting by signature', function () {
|
||||
it('supports EOA signatures', async function () {
|
||||
await this.token.connect(this.voter2).delegate(this.other);
|
||||
|
||||
// Run proposal
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
|
||||
// Prepare vote
|
||||
const weight = ethers.parseEther('7') - params.decoded[0];
|
||||
const nonce = await this.mock.nonces(this.other);
|
||||
const data = {
|
||||
proposalId: this.proposal.id,
|
||||
support: VoteType.For,
|
||||
voter: this.other.address,
|
||||
nonce,
|
||||
reason: 'no particular reason',
|
||||
params: params.encoded,
|
||||
signature: (contract, message) =>
|
||||
getDomain(contract).then(domain => this.other.signTypedData(domain, { ExtendedBallot }, message)),
|
||||
};
|
||||
|
||||
// Vote
|
||||
await expect(this.helper.vote(data))
|
||||
.to.emit(this.mock, 'CountParams')
|
||||
.withArgs(...params.decoded)
|
||||
.to.emit(this.mock, 'VoteCastWithParams')
|
||||
.withArgs(data.voter, data.proposalId, data.support, weight, data.reason, data.params);
|
||||
|
||||
expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([0n, weight, 0n]);
|
||||
expect(await this.mock.nonces(this.other)).to.equal(nonce + 1n);
|
||||
});
|
||||
|
||||
it('supports EIP-1271 signature signatures', async function () {
|
||||
const wallet = await ethers.deployContract('ERC1271WalletMock', [this.other]);
|
||||
await this.token.connect(this.voter2).delegate(wallet);
|
||||
|
||||
// Run proposal
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
|
||||
// Prepare vote
|
||||
const weight = ethers.parseEther('7') - params.decoded[0];
|
||||
const nonce = await this.mock.nonces(this.other);
|
||||
const data = {
|
||||
proposalId: this.proposal.id,
|
||||
support: VoteType.For,
|
||||
voter: wallet.target,
|
||||
nonce,
|
||||
reason: 'no particular reason',
|
||||
params: params.encoded,
|
||||
signature: (contract, message) =>
|
||||
getDomain(contract).then(domain => this.other.signTypedData(domain, { ExtendedBallot }, message)),
|
||||
};
|
||||
|
||||
// Vote
|
||||
await expect(this.helper.vote(data))
|
||||
.to.emit(this.mock, 'CountParams')
|
||||
.withArgs(...params.decoded)
|
||||
.to.emit(this.mock, 'VoteCastWithParams')
|
||||
.withArgs(data.voter, data.proposalId, data.support, weight, data.reason, data.params);
|
||||
|
||||
expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([0n, weight, 0n]);
|
||||
expect(await this.mock.nonces(wallet)).to.equal(nonce + 1n);
|
||||
});
|
||||
|
||||
it('reverts if signature does not match signer', async function () {
|
||||
await this.token.connect(this.voter2).delegate(this.other);
|
||||
|
||||
// Run proposal
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
|
||||
// Prepare vote
|
||||
const nonce = await this.mock.nonces(this.other);
|
||||
const data = {
|
||||
proposalId: this.proposal.id,
|
||||
support: VoteType.For,
|
||||
voter: this.other.address,
|
||||
nonce,
|
||||
reason: 'no particular reason',
|
||||
params: params.encoded,
|
||||
// tampered signature
|
||||
signature: (contract, message) =>
|
||||
getDomain(contract)
|
||||
.then(domain => this.other.signTypedData(domain, { ExtendedBallot }, message))
|
||||
.then(signature => {
|
||||
const tamperedSig = ethers.toBeArray(signature);
|
||||
tamperedSig[42] ^= 0xff;
|
||||
return ethers.hexlify(tamperedSig);
|
||||
}),
|
||||
};
|
||||
|
||||
// Vote
|
||||
await expect(this.helper.vote(data))
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorInvalidSignature')
|
||||
.withArgs(data.voter);
|
||||
});
|
||||
|
||||
it('reverts if vote nonce is incorrect', async function () {
|
||||
await this.token.connect(this.voter2).delegate(this.other);
|
||||
|
||||
// Run proposal
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
|
||||
// Prepare vote
|
||||
const nonce = await this.mock.nonces(this.other);
|
||||
const data = {
|
||||
proposalId: this.proposal.id,
|
||||
support: VoteType.For,
|
||||
voter: this.other.address,
|
||||
nonce: nonce + 1n,
|
||||
reason: 'no particular reason',
|
||||
params: params.encoded,
|
||||
signature: (contract, message) =>
|
||||
getDomain(contract).then(domain => this.other.signTypedData(domain, { ExtendedBallot }, message)),
|
||||
};
|
||||
|
||||
// Vote
|
||||
await expect(this.helper.vote(data))
|
||||
.to.be.revertedWithCustomError(this.mock, 'GovernorInvalidSignature')
|
||||
.withArgs(data.voter);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user