Governor.test.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
  2. const { expect } = require('chai');
  3. const ethSigUtil = require('eth-sig-util');
  4. const Wallet = require('ethereumjs-wallet').default;
  5. const { fromRpcSig } = require('ethereumjs-util');
  6. const Enums = require('../helpers/enums');
  7. const { EIP712Domain } = require('../helpers/eip712');
  8. const { GovernorHelper } = require('../helpers/governance');
  9. const {
  10. shouldSupportInterfaces,
  11. } = require('../utils/introspection/SupportsInterface.behavior');
  12. const Token = artifacts.require('ERC20VotesMock');
  13. const Governor = artifacts.require('GovernorMock');
  14. const CallReceiver = artifacts.require('CallReceiverMock');
  15. contract('Governor', function (accounts) {
  16. const [ owner, proposer, voter1, voter2, voter3, voter4 ] = accounts;
  17. const empty = web3.utils.toChecksumAddress(web3.utils.randomHex(20));
  18. const name = 'OZ-Governor';
  19. const version = '1';
  20. const tokenName = 'MockToken';
  21. const tokenSymbol = 'MTKN';
  22. const tokenSupply = web3.utils.toWei('100');
  23. const votingDelay = new BN(4);
  24. const votingPeriod = new BN(16);
  25. const value = web3.utils.toWei('1');
  26. beforeEach(async function () {
  27. this.chainId = await web3.eth.getChainId();
  28. this.token = await Token.new(tokenName, tokenSymbol);
  29. this.mock = await Governor.new(name, this.token.address, votingDelay, votingPeriod, 10);
  30. this.receiver = await CallReceiver.new();
  31. this.helper = new GovernorHelper(this.mock);
  32. await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value });
  33. await this.token.mint(owner, tokenSupply);
  34. await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
  35. await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
  36. await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
  37. await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
  38. this.proposal = this.helper.setProposal([
  39. {
  40. target: this.receiver.address,
  41. data: this.receiver.contract.methods.mockFunction().encodeABI(),
  42. value,
  43. },
  44. ], '<proposal description>');
  45. });
  46. shouldSupportInterfaces([
  47. 'ERC165',
  48. 'Governor',
  49. 'GovernorWithParams',
  50. ]);
  51. it('deployment check', async function () {
  52. expect(await this.mock.name()).to.be.equal(name);
  53. expect(await this.mock.token()).to.be.equal(this.token.address);
  54. expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay);
  55. expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
  56. expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
  57. expect(await this.mock.COUNTING_MODE()).to.be.equal('support=bravo&quorum=for,abstain');
  58. });
  59. it('nominal workflow', async function () {
  60. // Before
  61. expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
  62. expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false);
  63. expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false);
  64. expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value);
  65. expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0');
  66. // Run proposal
  67. const txPropose = await this.helper.propose({ from: proposer });
  68. expectEvent(
  69. txPropose,
  70. 'ProposalCreated',
  71. {
  72. proposalId: this.proposal.id,
  73. proposer,
  74. targets: this.proposal.targets,
  75. // values: this.proposal.values,
  76. signatures: this.proposal.signatures,
  77. calldatas: this.proposal.data,
  78. startBlock: new BN(txPropose.receipt.blockNumber).add(votingDelay),
  79. endBlock: new BN(txPropose.receipt.blockNumber).add(votingDelay).add(votingPeriod),
  80. description: this.proposal.description,
  81. },
  82. );
  83. await this.helper.waitForSnapshot();
  84. expectEvent(
  85. await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }),
  86. 'VoteCast',
  87. {
  88. voter: voter1,
  89. support: Enums.VoteType.For,
  90. reason: 'This is nice',
  91. weight: web3.utils.toWei('10'),
  92. },
  93. );
  94. expectEvent(
  95. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }),
  96. 'VoteCast',
  97. {
  98. voter: voter2,
  99. support: Enums.VoteType.For,
  100. weight: web3.utils.toWei('7'),
  101. },
  102. );
  103. expectEvent(
  104. await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }),
  105. 'VoteCast',
  106. {
  107. voter: voter3,
  108. support: Enums.VoteType.Against,
  109. weight: web3.utils.toWei('5'),
  110. },
  111. );
  112. expectEvent(
  113. await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }),
  114. 'VoteCast',
  115. {
  116. voter: voter4,
  117. support: Enums.VoteType.Abstain,
  118. weight: web3.utils.toWei('2'),
  119. },
  120. );
  121. await this.helper.waitForDeadline();
  122. const txExecute = await this.helper.execute();
  123. expectEvent(
  124. txExecute,
  125. 'ProposalExecuted',
  126. { proposalId: this.proposal.id },
  127. );
  128. await expectEvent.inTransaction(
  129. txExecute.tx,
  130. this.receiver,
  131. 'MockFunctionCalled',
  132. );
  133. // After
  134. expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
  135. expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true);
  136. expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true);
  137. expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0');
  138. expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(value);
  139. });
  140. it('vote with signature', async function () {
  141. const voterBySig = Wallet.generate();
  142. const voterBySigAddress = web3.utils.toChecksumAddress(voterBySig.getAddressString());
  143. const signature = async (message) => {
  144. return fromRpcSig(ethSigUtil.signTypedMessage(
  145. voterBySig.getPrivateKey(),
  146. {
  147. data: {
  148. types: {
  149. EIP712Domain,
  150. Ballot: [
  151. { name: 'proposalId', type: 'uint256' },
  152. { name: 'support', type: 'uint8' },
  153. ],
  154. },
  155. domain: { name, version, chainId: this.chainId, verifyingContract: this.mock.address },
  156. primaryType: 'Ballot',
  157. message,
  158. },
  159. },
  160. ));
  161. };
  162. await this.token.delegate(voterBySigAddress, { from: voter1 });
  163. // Run proposal
  164. await this.helper.propose();
  165. await this.helper.waitForSnapshot();
  166. expectEvent(
  167. await this.helper.vote({ support: Enums.VoteType.For, signature }),
  168. 'VoteCast',
  169. { voter: voterBySigAddress, support: Enums.VoteType.For },
  170. );
  171. await this.helper.waitForDeadline();
  172. await this.helper.execute();
  173. // After
  174. expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
  175. expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false);
  176. expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false);
  177. expect(await this.mock.hasVoted(this.proposal.id, voterBySigAddress)).to.be.equal(true);
  178. });
  179. it('send ethers', async function () {
  180. this.proposal = this.helper.setProposal([
  181. {
  182. target: empty,
  183. value,
  184. },
  185. ], '<proposal description>');
  186. // Before
  187. expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value);
  188. expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal('0');
  189. // Run proposal
  190. await this.helper.propose();
  191. await this.helper.waitForSnapshot();
  192. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  193. await this.helper.waitForDeadline();
  194. await this.helper.execute();
  195. // After
  196. expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0');
  197. expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal(value);
  198. });
  199. describe('should revert', function () {
  200. describe('on propose', function () {
  201. it('if proposal already exists', async function () {
  202. await this.helper.propose();
  203. await expectRevert(this.helper.propose(), 'Governor: proposal already exists');
  204. });
  205. });
  206. describe('on vote', function () {
  207. it('if proposal does not exist', async function () {
  208. await expectRevert(
  209. this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
  210. 'Governor: unknown proposal id',
  211. );
  212. });
  213. it('if voting has not started', async function () {
  214. await this.helper.propose();
  215. await expectRevert(
  216. this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
  217. 'Governor: vote not currently active',
  218. );
  219. });
  220. it('if support value is invalid', async function () {
  221. await this.helper.propose();
  222. await this.helper.waitForSnapshot();
  223. await expectRevert(
  224. this.helper.vote({ support: new BN('255') }),
  225. 'GovernorVotingSimple: invalid value for enum VoteType',
  226. );
  227. });
  228. it('if vote was already casted', async function () {
  229. await this.helper.propose();
  230. await this.helper.waitForSnapshot();
  231. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  232. await expectRevert(
  233. this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
  234. 'GovernorVotingSimple: vote already cast',
  235. );
  236. });
  237. it('if voting is over', async function () {
  238. await this.helper.propose();
  239. await this.helper.waitForDeadline();
  240. await expectRevert(
  241. this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
  242. 'Governor: vote not currently active',
  243. );
  244. });
  245. });
  246. describe('on execute', function () {
  247. it('if proposal does not exist', async function () {
  248. await expectRevert(this.helper.execute(), 'Governor: unknown proposal id');
  249. });
  250. it('if quorum is not reached', async function () {
  251. await this.helper.propose();
  252. await this.helper.waitForSnapshot();
  253. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter3 });
  254. await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
  255. });
  256. it('if score not reached', async function () {
  257. await this.helper.propose();
  258. await this.helper.waitForSnapshot();
  259. await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter1 });
  260. await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
  261. });
  262. it('if voting is not over', async function () {
  263. await this.helper.propose();
  264. await this.helper.waitForSnapshot();
  265. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  266. await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
  267. });
  268. it('if receiver revert without reason', async function () {
  269. this.proposal = this.helper.setProposal([
  270. {
  271. target: this.receiver.address,
  272. data: this.receiver.contract.methods.mockFunctionRevertsNoReason().encodeABI(),
  273. },
  274. ], '<proposal description>');
  275. await this.helper.propose();
  276. await this.helper.waitForSnapshot();
  277. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  278. await this.helper.waitForDeadline();
  279. await expectRevert(this.helper.execute(), 'Governor: call reverted without message');
  280. });
  281. it('if receiver revert with reason', async function () {
  282. this.proposal = this.helper.setProposal([
  283. {
  284. target: this.receiver.address,
  285. data: this.receiver.contract.methods.mockFunctionRevertsReason().encodeABI(),
  286. },
  287. ], '<proposal description>');
  288. await this.helper.propose();
  289. await this.helper.waitForSnapshot();
  290. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  291. await this.helper.waitForDeadline();
  292. await expectRevert(this.helper.execute(), 'CallReceiverMock: reverting');
  293. });
  294. it('if proposal was already executed', async function () {
  295. await this.helper.propose();
  296. await this.helper.waitForSnapshot();
  297. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  298. await this.helper.waitForDeadline();
  299. await this.helper.execute();
  300. await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
  301. });
  302. });
  303. });
  304. describe('state', function () {
  305. it('Unset', async function () {
  306. await expectRevert(this.mock.state(this.proposal.id), 'Governor: unknown proposal id');
  307. });
  308. it('Pending & Active', async function () {
  309. await this.helper.propose();
  310. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending);
  311. await this.helper.waitForSnapshot();
  312. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending);
  313. await this.helper.waitForSnapshot(+1);
  314. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
  315. });
  316. it('Defeated', async function () {
  317. await this.helper.propose();
  318. await this.helper.waitForDeadline();
  319. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
  320. await this.helper.waitForDeadline(+1);
  321. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated);
  322. });
  323. it('Succeeded', async function () {
  324. await this.helper.propose();
  325. await this.helper.waitForSnapshot();
  326. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  327. await this.helper.waitForDeadline();
  328. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
  329. await this.helper.waitForDeadline(+1);
  330. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded);
  331. });
  332. it('Executed', async function () {
  333. await this.helper.propose();
  334. await this.helper.waitForSnapshot();
  335. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  336. await this.helper.waitForDeadline();
  337. await this.helper.execute();
  338. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Executed);
  339. });
  340. });
  341. describe('cancel', function () {
  342. it('before proposal', async function () {
  343. await expectRevert(this.helper.cancel(), 'Governor: unknown proposal id');
  344. });
  345. it('after proposal', async function () {
  346. await this.helper.propose();
  347. await this.helper.cancel();
  348. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
  349. await this.helper.waitForSnapshot();
  350. await expectRevert(
  351. this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
  352. 'Governor: vote not currently active',
  353. );
  354. });
  355. it('after vote', async function () {
  356. await this.helper.propose();
  357. await this.helper.waitForSnapshot();
  358. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  359. await this.helper.cancel();
  360. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
  361. await this.helper.waitForDeadline();
  362. await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
  363. });
  364. it('after deadline', async function () {
  365. await this.helper.propose();
  366. await this.helper.waitForSnapshot();
  367. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  368. await this.helper.waitForDeadline();
  369. await this.helper.cancel();
  370. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
  371. await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
  372. });
  373. it('after execution', async function () {
  374. await this.helper.propose();
  375. await this.helper.waitForSnapshot();
  376. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  377. await this.helper.waitForDeadline();
  378. await this.helper.execute();
  379. await expectRevert(this.helper.cancel(), 'Governor: proposal not active');
  380. });
  381. });
  382. describe('proposal length', function () {
  383. it('empty', async function () {
  384. this.helper.setProposal([ ], '<proposal description>');
  385. await expectRevert(this.helper.propose(), 'Governor: empty proposal');
  386. });
  387. it('missmatch #1', async function () {
  388. this.helper.setProposal({
  389. targets: [ ],
  390. values: [ web3.utils.toWei('0') ],
  391. data: [ this.receiver.contract.methods.mockFunction().encodeABI() ],
  392. }, '<proposal description>');
  393. await expectRevert(this.helper.propose(), 'Governor: invalid proposal length');
  394. });
  395. it('missmatch #2', async function () {
  396. this.helper.setProposal({
  397. targets: [ this.receiver.address ],
  398. values: [ ],
  399. data: [ this.receiver.contract.methods.mockFunction().encodeABI() ],
  400. }, '<proposal description>');
  401. await expectRevert(this.helper.propose(), 'Governor: invalid proposal length');
  402. });
  403. it('missmatch #3', async function () {
  404. this.helper.setProposal({
  405. targets: [ this.receiver.address ],
  406. values: [ web3.utils.toWei('0') ],
  407. data: [ ],
  408. }, '<proposal description>');
  409. await expectRevert(this.helper.propose(), 'Governor: invalid proposal length');
  410. });
  411. });
  412. describe('onlyGovernance updates', function () {
  413. it('setVotingDelay is protected', async function () {
  414. await expectRevert(this.mock.setVotingDelay('0'), 'Governor: onlyGovernance');
  415. });
  416. it('setVotingPeriod is protected', async function () {
  417. await expectRevert(this.mock.setVotingPeriod('32'), 'Governor: onlyGovernance');
  418. });
  419. it('setProposalThreshold is protected', async function () {
  420. await expectRevert(this.mock.setProposalThreshold('1000000000000000000'), 'Governor: onlyGovernance');
  421. });
  422. it('can setVotingDelay through governance', async function () {
  423. this.helper.setProposal([
  424. {
  425. target: this.mock.address,
  426. data: this.mock.contract.methods.setVotingDelay('0').encodeABI(),
  427. },
  428. ], '<proposal description>');
  429. await this.helper.propose();
  430. await this.helper.waitForSnapshot();
  431. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  432. await this.helper.waitForDeadline();
  433. expectEvent(
  434. await this.helper.execute(),
  435. 'VotingDelaySet',
  436. { oldVotingDelay: '4', newVotingDelay: '0' },
  437. );
  438. expect(await this.mock.votingDelay()).to.be.bignumber.equal('0');
  439. });
  440. it('can setVotingPeriod through governance', async function () {
  441. this.helper.setProposal([
  442. {
  443. target: this.mock.address,
  444. data: this.mock.contract.methods.setVotingPeriod('32').encodeABI(),
  445. },
  446. ], '<proposal description>');
  447. await this.helper.propose();
  448. await this.helper.waitForSnapshot();
  449. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  450. await this.helper.waitForDeadline();
  451. expectEvent(
  452. await this.helper.execute(),
  453. 'VotingPeriodSet',
  454. { oldVotingPeriod: '16', newVotingPeriod: '32' },
  455. );
  456. expect(await this.mock.votingPeriod()).to.be.bignumber.equal('32');
  457. });
  458. it('cannot setVotingPeriod to 0 through governance', async function () {
  459. this.helper.setProposal([
  460. {
  461. target: this.mock.address,
  462. data: this.mock.contract.methods.setVotingPeriod('0').encodeABI(),
  463. },
  464. ], '<proposal description>');
  465. await this.helper.propose();
  466. await this.helper.waitForSnapshot();
  467. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  468. await this.helper.waitForDeadline();
  469. await expectRevert(this.helper.execute(), 'GovernorSettings: voting period too low');
  470. });
  471. it('can setProposalThreshold to 0 through governance', async function () {
  472. this.helper.setProposal([
  473. {
  474. target: this.mock.address,
  475. data: this.mock.contract.methods.setProposalThreshold('1000000000000000000').encodeABI(),
  476. },
  477. ], '<proposal description>');
  478. await this.helper.propose();
  479. await this.helper.waitForSnapshot();
  480. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  481. await this.helper.waitForDeadline();
  482. expectEvent(
  483. await this.helper.execute(),
  484. 'ProposalThresholdSet',
  485. { oldProposalThreshold: '0', newProposalThreshold: '1000000000000000000' },
  486. );
  487. expect(await this.mock.proposalThreshold()).to.be.bignumber.equal('1000000000000000000');
  488. });
  489. });
  490. });