Governor.test.js 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018
  1. const { constants, 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 Enums = require('../helpers/enums');
  6. const {
  7. getDomain,
  8. domainType,
  9. types: { Ballot },
  10. } = require('../helpers/eip712');
  11. const { GovernorHelper, proposalStatesToBitMap } = require('../helpers/governance');
  12. const { clockFromReceipt } = require('../helpers/time');
  13. const { expectRevertCustomError } = require('../helpers/customError');
  14. const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior');
  15. const { shouldBehaveLikeERC6372 } = require('./utils/ERC6372.behavior');
  16. const { ZERO_BYTES32 } = require('@openzeppelin/test-helpers/src/constants');
  17. const Governor = artifacts.require('$GovernorMock');
  18. const CallReceiver = artifacts.require('CallReceiverMock');
  19. const ERC721 = artifacts.require('$ERC721');
  20. const ERC1155 = artifacts.require('$ERC1155');
  21. const ERC1271WalletMock = artifacts.require('ERC1271WalletMock');
  22. const TOKENS = [
  23. { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' },
  24. { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' },
  25. { Token: artifacts.require('$ERC20VotesLegacyMock'), mode: 'blocknumber' },
  26. ];
  27. contract('Governor', function (accounts) {
  28. const [owner, proposer, voter1, voter2, voter3, voter4] = accounts;
  29. const name = 'OZ-Governor';
  30. const version = '1';
  31. const tokenName = 'MockToken';
  32. const tokenSymbol = 'MTKN';
  33. const tokenSupply = web3.utils.toWei('100');
  34. const votingDelay = web3.utils.toBN(4);
  35. const votingPeriod = web3.utils.toBN(16);
  36. const value = web3.utils.toWei('1');
  37. for (const { mode, Token } of TOKENS) {
  38. describe(`using ${Token._json.contractName}`, function () {
  39. beforeEach(async function () {
  40. this.chainId = await web3.eth.getChainId();
  41. try {
  42. this.token = await Token.new(tokenName, tokenSymbol, tokenName, version);
  43. } catch {
  44. // ERC20VotesLegacyMock has a different construction that uses version='1' by default.
  45. this.token = await Token.new(tokenName, tokenSymbol, tokenName);
  46. }
  47. this.mock = await Governor.new(
  48. name, // name
  49. votingDelay, // initialVotingDelay
  50. votingPeriod, // initialVotingPeriod
  51. 0, // initialProposalThreshold
  52. this.token.address, // tokenAddress
  53. 10, // quorumNumeratorValue
  54. );
  55. this.receiver = await CallReceiver.new();
  56. this.helper = new GovernorHelper(this.mock, mode);
  57. await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value });
  58. await this.token.$_mint(owner, tokenSupply);
  59. await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
  60. await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
  61. await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
  62. await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
  63. this.proposal = this.helper.setProposal(
  64. [
  65. {
  66. target: this.receiver.address,
  67. data: this.receiver.contract.methods.mockFunction().encodeABI(),
  68. value,
  69. },
  70. ],
  71. '<proposal description>',
  72. );
  73. });
  74. shouldSupportInterfaces(['ERC165', 'ERC1155Receiver', 'Governor']);
  75. shouldBehaveLikeERC6372(mode);
  76. it('deployment check', async function () {
  77. expect(await this.mock.name()).to.be.equal(name);
  78. expect(await this.mock.token()).to.be.equal(this.token.address);
  79. expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay);
  80. expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
  81. expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
  82. expect(await this.mock.COUNTING_MODE()).to.be.equal('support=bravo&quorum=for,abstain');
  83. });
  84. it('nominal workflow', async function () {
  85. // Before
  86. expect(await this.mock.proposalProposer(this.proposal.id)).to.be.equal(constants.ZERO_ADDRESS);
  87. expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
  88. expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false);
  89. expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false);
  90. expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value);
  91. expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0');
  92. expect(await this.mock.proposalEta(this.proposal.id)).to.be.bignumber.equal('0');
  93. expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.equal(false);
  94. // Run proposal
  95. const txPropose = await this.helper.propose({ from: proposer });
  96. expectEvent(txPropose, 'ProposalCreated', {
  97. proposalId: this.proposal.id,
  98. proposer,
  99. targets: this.proposal.targets,
  100. // values: this.proposal.values,
  101. signatures: this.proposal.signatures,
  102. calldatas: this.proposal.data,
  103. voteStart: web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay),
  104. voteEnd: web3.utils
  105. .toBN(await clockFromReceipt[mode](txPropose.receipt))
  106. .add(votingDelay)
  107. .add(votingPeriod),
  108. description: this.proposal.description,
  109. });
  110. await this.helper.waitForSnapshot();
  111. expectEvent(
  112. await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }),
  113. 'VoteCast',
  114. {
  115. voter: voter1,
  116. support: Enums.VoteType.For,
  117. reason: 'This is nice',
  118. weight: web3.utils.toWei('10'),
  119. },
  120. );
  121. expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }), 'VoteCast', {
  122. voter: voter2,
  123. support: Enums.VoteType.For,
  124. weight: web3.utils.toWei('7'),
  125. });
  126. expectEvent(await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }), 'VoteCast', {
  127. voter: voter3,
  128. support: Enums.VoteType.Against,
  129. weight: web3.utils.toWei('5'),
  130. });
  131. expectEvent(await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }), 'VoteCast', {
  132. voter: voter4,
  133. support: Enums.VoteType.Abstain,
  134. weight: web3.utils.toWei('2'),
  135. });
  136. await this.helper.waitForDeadline();
  137. const txExecute = await this.helper.execute();
  138. expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id });
  139. await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
  140. // After
  141. expect(await this.mock.proposalProposer(this.proposal.id)).to.be.equal(proposer);
  142. expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
  143. expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true);
  144. expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true);
  145. expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0');
  146. expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(value);
  147. expect(await this.mock.proposalEta(this.proposal.id)).to.be.bignumber.equal('0');
  148. expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.equal(false);
  149. });
  150. it('send ethers', async function () {
  151. const empty = web3.utils.toChecksumAddress(web3.utils.randomHex(20));
  152. this.proposal = this.helper.setProposal(
  153. [
  154. {
  155. target: empty,
  156. value,
  157. },
  158. ],
  159. '<proposal description>',
  160. );
  161. // Before
  162. expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value);
  163. expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal('0');
  164. // Run proposal
  165. await this.helper.propose();
  166. await this.helper.waitForSnapshot();
  167. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  168. await this.helper.waitForDeadline();
  169. await this.helper.execute();
  170. // After
  171. expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0');
  172. expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal(value);
  173. });
  174. describe('vote with signature', function () {
  175. const sign = privateKey => async (contract, message) => {
  176. const domain = await getDomain(contract);
  177. return ethSigUtil.signTypedMessage(privateKey, {
  178. data: {
  179. primaryType: 'Ballot',
  180. types: {
  181. EIP712Domain: domainType(domain),
  182. Ballot,
  183. },
  184. domain,
  185. message,
  186. },
  187. });
  188. };
  189. afterEach('no other votes are cast for proposalId', async function () {
  190. expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
  191. expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false);
  192. expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false);
  193. });
  194. it('votes with an EOA signature', async function () {
  195. const voterBySig = Wallet.generate();
  196. const voterBySigAddress = web3.utils.toChecksumAddress(voterBySig.getAddressString());
  197. await this.token.delegate(voterBySigAddress, { from: voter1 });
  198. const nonce = await this.mock.nonces(voterBySigAddress);
  199. // Run proposal
  200. await this.helper.propose();
  201. await this.helper.waitForSnapshot();
  202. expectEvent(
  203. await this.helper.vote({
  204. support: Enums.VoteType.For,
  205. voter: voterBySigAddress,
  206. nonce,
  207. signature: sign(voterBySig.getPrivateKey()),
  208. }),
  209. 'VoteCast',
  210. {
  211. voter: voterBySigAddress,
  212. support: Enums.VoteType.For,
  213. },
  214. );
  215. await this.helper.waitForDeadline();
  216. await this.helper.execute();
  217. // After
  218. expect(await this.mock.hasVoted(this.proposal.id, voterBySigAddress)).to.be.equal(true);
  219. expect(await this.mock.nonces(voterBySigAddress)).to.be.bignumber.equal(nonce.addn(1));
  220. });
  221. it('votes with a valid EIP-1271 signature', async function () {
  222. const ERC1271WalletOwner = Wallet.generate();
  223. ERC1271WalletOwner.address = web3.utils.toChecksumAddress(ERC1271WalletOwner.getAddressString());
  224. const wallet = await ERC1271WalletMock.new(ERC1271WalletOwner.address);
  225. await this.token.delegate(wallet.address, { from: voter1 });
  226. const nonce = await this.mock.nonces(wallet.address);
  227. // Run proposal
  228. await this.helper.propose();
  229. await this.helper.waitForSnapshot();
  230. expectEvent(
  231. await this.helper.vote({
  232. support: Enums.VoteType.For,
  233. voter: wallet.address,
  234. nonce,
  235. signature: sign(ERC1271WalletOwner.getPrivateKey()),
  236. }),
  237. 'VoteCast',
  238. {
  239. voter: wallet.address,
  240. support: Enums.VoteType.For,
  241. },
  242. );
  243. await this.helper.waitForDeadline();
  244. await this.helper.execute();
  245. // After
  246. expect(await this.mock.hasVoted(this.proposal.id, wallet.address)).to.be.equal(true);
  247. expect(await this.mock.nonces(wallet.address)).to.be.bignumber.equal(nonce.addn(1));
  248. });
  249. afterEach('no other votes are cast', async function () {
  250. expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
  251. expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false);
  252. expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false);
  253. });
  254. });
  255. describe('should revert', function () {
  256. describe('on propose', function () {
  257. it('if proposal already exists', async function () {
  258. await this.helper.propose();
  259. await expectRevertCustomError(this.helper.propose(), 'GovernorUnexpectedProposalState', [
  260. this.proposal.id,
  261. Enums.ProposalState.Pending,
  262. ZERO_BYTES32,
  263. ]);
  264. });
  265. it('if proposer has below threshold votes', async function () {
  266. const votes = web3.utils.toWei('10');
  267. const threshold = web3.utils.toWei('1000');
  268. await this.mock.$_setProposalThreshold(threshold);
  269. await expectRevertCustomError(this.helper.propose({ from: voter1 }), 'GovernorInsufficientProposerVotes', [
  270. voter1,
  271. votes,
  272. threshold,
  273. ]);
  274. });
  275. });
  276. describe('on vote', function () {
  277. it('if proposal does not exist', async function () {
  278. await expectRevertCustomError(
  279. this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
  280. 'GovernorNonexistentProposal',
  281. [this.proposal.id],
  282. );
  283. });
  284. it('if voting has not started', async function () {
  285. await this.helper.propose();
  286. await expectRevertCustomError(
  287. this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
  288. 'GovernorUnexpectedProposalState',
  289. [this.proposal.id, Enums.ProposalState.Pending, proposalStatesToBitMap([Enums.ProposalState.Active])],
  290. );
  291. });
  292. it('if support value is invalid', async function () {
  293. await this.helper.propose();
  294. await this.helper.waitForSnapshot();
  295. await expectRevertCustomError(
  296. this.helper.vote({ support: web3.utils.toBN('255') }),
  297. 'GovernorInvalidVoteType',
  298. [],
  299. );
  300. });
  301. it('if vote was already casted', async function () {
  302. await this.helper.propose();
  303. await this.helper.waitForSnapshot();
  304. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  305. await expectRevertCustomError(
  306. this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
  307. 'GovernorAlreadyCastVote',
  308. [voter1],
  309. );
  310. });
  311. it('if voting is over', async function () {
  312. await this.helper.propose();
  313. await this.helper.waitForDeadline();
  314. await expectRevertCustomError(
  315. this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
  316. 'GovernorUnexpectedProposalState',
  317. [this.proposal.id, Enums.ProposalState.Defeated, proposalStatesToBitMap([Enums.ProposalState.Active])],
  318. );
  319. });
  320. });
  321. describe('on vote by signature', function () {
  322. beforeEach(async function () {
  323. this.voterBySig = Wallet.generate();
  324. this.voterBySig.address = web3.utils.toChecksumAddress(this.voterBySig.getAddressString());
  325. this.data = (contract, message) =>
  326. getDomain(contract).then(domain => ({
  327. primaryType: 'Ballot',
  328. types: {
  329. EIP712Domain: domainType(domain),
  330. Ballot,
  331. },
  332. domain,
  333. message,
  334. }));
  335. this.signature = (contract, message) =>
  336. this.data(contract, message).then(data =>
  337. ethSigUtil.signTypedMessage(this.voterBySig.getPrivateKey(), { data }),
  338. );
  339. await this.token.delegate(this.voterBySig.address, { from: voter1 });
  340. // Run proposal
  341. await this.helper.propose();
  342. await this.helper.waitForSnapshot();
  343. });
  344. it('if signature does not match signer', async function () {
  345. const nonce = await this.mock.nonces(this.voterBySig.address);
  346. const voteParams = {
  347. support: Enums.VoteType.For,
  348. voter: this.voterBySig.address,
  349. nonce,
  350. signature: async (...params) => {
  351. const sig = await this.signature(...params);
  352. const tamperedSig = web3.utils.hexToBytes(sig);
  353. tamperedSig[42] ^= 0xff;
  354. return web3.utils.bytesToHex(tamperedSig);
  355. },
  356. };
  357. await expectRevertCustomError(this.helper.vote(voteParams), 'GovernorInvalidSignature', [voteParams.voter]);
  358. });
  359. it('if vote nonce is incorrect', async function () {
  360. const nonce = await this.mock.nonces(this.voterBySig.address);
  361. const voteParams = {
  362. support: Enums.VoteType.For,
  363. voter: this.voterBySig.address,
  364. nonce: nonce.addn(1),
  365. signature: this.signature,
  366. };
  367. await expectRevertCustomError(
  368. this.helper.vote(voteParams),
  369. // The signature check implies the nonce can't be tampered without changing the signer
  370. 'GovernorInvalidSignature',
  371. [voteParams.voter],
  372. );
  373. });
  374. });
  375. describe('on queue', function () {
  376. it('always', async function () {
  377. await this.helper.propose({ from: proposer });
  378. await this.helper.waitForSnapshot();
  379. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  380. await this.helper.waitForDeadline();
  381. await expectRevertCustomError(this.helper.queue(), 'GovernorQueueNotImplemented', []);
  382. });
  383. });
  384. describe('on execute', function () {
  385. it('if proposal does not exist', async function () {
  386. await expectRevertCustomError(this.helper.execute(), 'GovernorNonexistentProposal', [this.proposal.id]);
  387. });
  388. it('if quorum is not reached', async function () {
  389. await this.helper.propose();
  390. await this.helper.waitForSnapshot();
  391. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter3 });
  392. await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [
  393. this.proposal.id,
  394. Enums.ProposalState.Active,
  395. proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]),
  396. ]);
  397. });
  398. it('if score not reached', async function () {
  399. await this.helper.propose();
  400. await this.helper.waitForSnapshot();
  401. await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter1 });
  402. await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [
  403. this.proposal.id,
  404. Enums.ProposalState.Active,
  405. proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]),
  406. ]);
  407. });
  408. it('if voting is not over', async function () {
  409. await this.helper.propose();
  410. await this.helper.waitForSnapshot();
  411. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  412. await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [
  413. this.proposal.id,
  414. Enums.ProposalState.Active,
  415. proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]),
  416. ]);
  417. });
  418. it('if receiver revert without reason', async function () {
  419. this.helper.setProposal(
  420. [
  421. {
  422. target: this.receiver.address,
  423. data: this.receiver.contract.methods.mockFunctionRevertsNoReason().encodeABI(),
  424. },
  425. ],
  426. '<proposal description>',
  427. );
  428. await this.helper.propose();
  429. await this.helper.waitForSnapshot();
  430. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  431. await this.helper.waitForDeadline();
  432. await expectRevertCustomError(this.helper.execute(), 'FailedInnerCall', []);
  433. });
  434. it('if receiver revert with reason', async function () {
  435. this.helper.setProposal(
  436. [
  437. {
  438. target: this.receiver.address,
  439. data: this.receiver.contract.methods.mockFunctionRevertsReason().encodeABI(),
  440. },
  441. ],
  442. '<proposal description>',
  443. );
  444. await this.helper.propose();
  445. await this.helper.waitForSnapshot();
  446. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  447. await this.helper.waitForDeadline();
  448. await expectRevert(this.helper.execute(), 'CallReceiverMock: reverting');
  449. });
  450. it('if proposal was already executed', async function () {
  451. await this.helper.propose();
  452. await this.helper.waitForSnapshot();
  453. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  454. await this.helper.waitForDeadline();
  455. await this.helper.execute();
  456. await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [
  457. this.proposal.id,
  458. Enums.ProposalState.Executed,
  459. proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]),
  460. ]);
  461. });
  462. });
  463. });
  464. describe('state', function () {
  465. it('Unset', async function () {
  466. await expectRevertCustomError(this.mock.state(this.proposal.id), 'GovernorNonexistentProposal', [
  467. this.proposal.id,
  468. ]);
  469. });
  470. it('Pending & Active', async function () {
  471. await this.helper.propose();
  472. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending);
  473. await this.helper.waitForSnapshot();
  474. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending);
  475. await this.helper.waitForSnapshot(+1);
  476. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
  477. });
  478. it('Defeated', async function () {
  479. await this.helper.propose();
  480. await this.helper.waitForDeadline();
  481. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
  482. await this.helper.waitForDeadline(+1);
  483. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated);
  484. });
  485. it('Succeeded', async function () {
  486. await this.helper.propose();
  487. await this.helper.waitForSnapshot();
  488. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  489. await this.helper.waitForDeadline();
  490. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
  491. await this.helper.waitForDeadline(+1);
  492. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded);
  493. });
  494. it('Executed', async function () {
  495. await this.helper.propose();
  496. await this.helper.waitForSnapshot();
  497. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  498. await this.helper.waitForDeadline();
  499. await this.helper.execute();
  500. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Executed);
  501. });
  502. });
  503. describe('cancel', function () {
  504. describe('internal', function () {
  505. it('before proposal', async function () {
  506. await expectRevertCustomError(this.helper.cancel('internal'), 'GovernorNonexistentProposal', [
  507. this.proposal.id,
  508. ]);
  509. });
  510. it('after proposal', async function () {
  511. await this.helper.propose();
  512. await this.helper.cancel('internal');
  513. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
  514. await this.helper.waitForSnapshot();
  515. await expectRevertCustomError(
  516. this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
  517. 'GovernorUnexpectedProposalState',
  518. [this.proposal.id, Enums.ProposalState.Canceled, proposalStatesToBitMap([Enums.ProposalState.Active])],
  519. );
  520. });
  521. it('after vote', async function () {
  522. await this.helper.propose();
  523. await this.helper.waitForSnapshot();
  524. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  525. await this.helper.cancel('internal');
  526. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
  527. await this.helper.waitForDeadline();
  528. await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [
  529. this.proposal.id,
  530. Enums.ProposalState.Canceled,
  531. proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]),
  532. ]);
  533. });
  534. it('after deadline', async function () {
  535. await this.helper.propose();
  536. await this.helper.waitForSnapshot();
  537. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  538. await this.helper.waitForDeadline();
  539. await this.helper.cancel('internal');
  540. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
  541. await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [
  542. this.proposal.id,
  543. Enums.ProposalState.Canceled,
  544. proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]),
  545. ]);
  546. });
  547. it('after execution', async function () {
  548. await this.helper.propose();
  549. await this.helper.waitForSnapshot();
  550. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  551. await this.helper.waitForDeadline();
  552. await this.helper.execute();
  553. await expectRevertCustomError(this.helper.cancel('internal'), 'GovernorUnexpectedProposalState', [
  554. this.proposal.id,
  555. Enums.ProposalState.Executed,
  556. proposalStatesToBitMap(
  557. [Enums.ProposalState.Canceled, Enums.ProposalState.Expired, Enums.ProposalState.Executed],
  558. { inverted: true },
  559. ),
  560. ]);
  561. });
  562. });
  563. describe('public', function () {
  564. it('before proposal', async function () {
  565. await expectRevertCustomError(this.helper.cancel('external'), 'GovernorNonexistentProposal', [
  566. this.proposal.id,
  567. ]);
  568. });
  569. it('after proposal', async function () {
  570. await this.helper.propose();
  571. await this.helper.cancel('external');
  572. });
  573. it('after proposal - restricted to proposer', async function () {
  574. await this.helper.propose();
  575. await expectRevertCustomError(this.helper.cancel('external', { from: owner }), 'GovernorOnlyProposer', [
  576. owner,
  577. ]);
  578. });
  579. it('after vote started', async function () {
  580. await this.helper.propose();
  581. await this.helper.waitForSnapshot(1); // snapshot + 1 block
  582. await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [
  583. this.proposal.id,
  584. Enums.ProposalState.Active,
  585. proposalStatesToBitMap([Enums.ProposalState.Pending]),
  586. ]);
  587. });
  588. it('after vote', async function () {
  589. await this.helper.propose();
  590. await this.helper.waitForSnapshot();
  591. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  592. await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [
  593. this.proposal.id,
  594. Enums.ProposalState.Active,
  595. proposalStatesToBitMap([Enums.ProposalState.Pending]),
  596. ]);
  597. });
  598. it('after deadline', async function () {
  599. await this.helper.propose();
  600. await this.helper.waitForSnapshot();
  601. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  602. await this.helper.waitForDeadline();
  603. await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [
  604. this.proposal.id,
  605. Enums.ProposalState.Succeeded,
  606. proposalStatesToBitMap([Enums.ProposalState.Pending]),
  607. ]);
  608. });
  609. it('after execution', async function () {
  610. await this.helper.propose();
  611. await this.helper.waitForSnapshot();
  612. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  613. await this.helper.waitForDeadline();
  614. await this.helper.execute();
  615. await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [
  616. this.proposal.id,
  617. Enums.ProposalState.Executed,
  618. proposalStatesToBitMap([Enums.ProposalState.Pending]),
  619. ]);
  620. });
  621. });
  622. });
  623. describe('proposal length', function () {
  624. it('empty', async function () {
  625. this.helper.setProposal([], '<proposal description>');
  626. await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [0, 0, 0]);
  627. });
  628. it('mismatch #1', async function () {
  629. this.helper.setProposal(
  630. {
  631. targets: [],
  632. values: [web3.utils.toWei('0')],
  633. data: [this.receiver.contract.methods.mockFunction().encodeABI()],
  634. },
  635. '<proposal description>',
  636. );
  637. await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [0, 1, 1]);
  638. });
  639. it('mismatch #2', async function () {
  640. this.helper.setProposal(
  641. {
  642. targets: [this.receiver.address],
  643. values: [],
  644. data: [this.receiver.contract.methods.mockFunction().encodeABI()],
  645. },
  646. '<proposal description>',
  647. );
  648. await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [1, 1, 0]);
  649. });
  650. it('mismatch #3', async function () {
  651. this.helper.setProposal(
  652. {
  653. targets: [this.receiver.address],
  654. values: [web3.utils.toWei('0')],
  655. data: [],
  656. },
  657. '<proposal description>',
  658. );
  659. await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [1, 0, 1]);
  660. });
  661. });
  662. describe('frontrun protection using description suffix', function () {
  663. describe('without protection', function () {
  664. describe('without suffix', function () {
  665. it('proposer can propose', async function () {
  666. expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated');
  667. });
  668. it('someone else can propose', async function () {
  669. expectEvent(await this.helper.propose({ from: voter1 }), 'ProposalCreated');
  670. });
  671. });
  672. describe('with different suffix', function () {
  673. beforeEach(async function () {
  674. this.proposal = this.helper.setProposal(
  675. [
  676. {
  677. target: this.receiver.address,
  678. data: this.receiver.contract.methods.mockFunction().encodeABI(),
  679. value,
  680. },
  681. ],
  682. `<proposal description>#wrong-suffix=${proposer}`,
  683. );
  684. });
  685. it('proposer can propose', async function () {
  686. expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated');
  687. });
  688. it('someone else can propose', async function () {
  689. expectEvent(await this.helper.propose({ from: voter1 }), 'ProposalCreated');
  690. });
  691. });
  692. describe('with proposer suffix but bad address part', function () {
  693. beforeEach(async function () {
  694. this.proposal = this.helper.setProposal(
  695. [
  696. {
  697. target: this.receiver.address,
  698. data: this.receiver.contract.methods.mockFunction().encodeABI(),
  699. value,
  700. },
  701. ],
  702. `<proposal description>#proposer=0x3C44CdDdB6a900fa2b585dd299e03d12FA429XYZ`, // XYZ are not a valid hex char
  703. );
  704. });
  705. it('propose can propose', async function () {
  706. expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated');
  707. });
  708. it('someone else can propose', async function () {
  709. expectEvent(await this.helper.propose({ from: voter1 }), 'ProposalCreated');
  710. });
  711. });
  712. });
  713. describe('with protection via proposer suffix', function () {
  714. beforeEach(async function () {
  715. this.proposal = this.helper.setProposal(
  716. [
  717. {
  718. target: this.receiver.address,
  719. data: this.receiver.contract.methods.mockFunction().encodeABI(),
  720. value,
  721. },
  722. ],
  723. `<proposal description>#proposer=${proposer}`,
  724. );
  725. });
  726. it('proposer can propose', async function () {
  727. expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated');
  728. });
  729. it('someone else cannot propose', async function () {
  730. await expectRevertCustomError(this.helper.propose({ from: voter1 }), 'GovernorRestrictedProposer', [
  731. voter1,
  732. ]);
  733. });
  734. });
  735. });
  736. describe('onlyGovernance updates', function () {
  737. it('setVotingDelay is protected', async function () {
  738. await expectRevertCustomError(this.mock.setVotingDelay('0', { from: owner }), 'GovernorOnlyExecutor', [
  739. owner,
  740. ]);
  741. });
  742. it('setVotingPeriod is protected', async function () {
  743. await expectRevertCustomError(this.mock.setVotingPeriod('32', { from: owner }), 'GovernorOnlyExecutor', [
  744. owner,
  745. ]);
  746. });
  747. it('setProposalThreshold is protected', async function () {
  748. await expectRevertCustomError(
  749. this.mock.setProposalThreshold('1000000000000000000', { from: owner }),
  750. 'GovernorOnlyExecutor',
  751. [owner],
  752. );
  753. });
  754. it('can setVotingDelay through governance', async function () {
  755. this.helper.setProposal(
  756. [
  757. {
  758. target: this.mock.address,
  759. data: this.mock.contract.methods.setVotingDelay('0').encodeABI(),
  760. },
  761. ],
  762. '<proposal description>',
  763. );
  764. await this.helper.propose();
  765. await this.helper.waitForSnapshot();
  766. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  767. await this.helper.waitForDeadline();
  768. expectEvent(await this.helper.execute(), 'VotingDelaySet', { oldVotingDelay: '4', newVotingDelay: '0' });
  769. expect(await this.mock.votingDelay()).to.be.bignumber.equal('0');
  770. });
  771. it('can setVotingPeriod through governance', async function () {
  772. this.helper.setProposal(
  773. [
  774. {
  775. target: this.mock.address,
  776. data: this.mock.contract.methods.setVotingPeriod('32').encodeABI(),
  777. },
  778. ],
  779. '<proposal description>',
  780. );
  781. await this.helper.propose();
  782. await this.helper.waitForSnapshot();
  783. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  784. await this.helper.waitForDeadline();
  785. expectEvent(await this.helper.execute(), 'VotingPeriodSet', { oldVotingPeriod: '16', newVotingPeriod: '32' });
  786. expect(await this.mock.votingPeriod()).to.be.bignumber.equal('32');
  787. });
  788. it('cannot setVotingPeriod to 0 through governance', async function () {
  789. const votingPeriod = 0;
  790. this.helper.setProposal(
  791. [
  792. {
  793. target: this.mock.address,
  794. data: this.mock.contract.methods.setVotingPeriod(votingPeriod).encodeABI(),
  795. },
  796. ],
  797. '<proposal description>',
  798. );
  799. await this.helper.propose();
  800. await this.helper.waitForSnapshot();
  801. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  802. await this.helper.waitForDeadline();
  803. await expectRevertCustomError(this.helper.execute(), 'GovernorInvalidVotingPeriod', [votingPeriod]);
  804. });
  805. it('can setProposalThreshold to 0 through governance', async function () {
  806. this.helper.setProposal(
  807. [
  808. {
  809. target: this.mock.address,
  810. data: this.mock.contract.methods.setProposalThreshold('1000000000000000000').encodeABI(),
  811. },
  812. ],
  813. '<proposal description>',
  814. );
  815. await this.helper.propose();
  816. await this.helper.waitForSnapshot();
  817. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  818. await this.helper.waitForDeadline();
  819. expectEvent(await this.helper.execute(), 'ProposalThresholdSet', {
  820. oldProposalThreshold: '0',
  821. newProposalThreshold: '1000000000000000000',
  822. });
  823. expect(await this.mock.proposalThreshold()).to.be.bignumber.equal('1000000000000000000');
  824. });
  825. });
  826. describe('safe receive', function () {
  827. describe('ERC721', function () {
  828. const name = 'Non Fungible Token';
  829. const symbol = 'NFT';
  830. const tokenId = web3.utils.toBN(1);
  831. beforeEach(async function () {
  832. this.token = await ERC721.new(name, symbol);
  833. await this.token.$_mint(owner, tokenId);
  834. });
  835. it('can receive an ERC721 safeTransfer', async function () {
  836. await this.token.safeTransferFrom(owner, this.mock.address, tokenId, { from: owner });
  837. });
  838. });
  839. describe('ERC1155', function () {
  840. const uri = 'https://token-cdn-domain/{id}.json';
  841. const tokenIds = {
  842. 1: web3.utils.toBN(1000),
  843. 2: web3.utils.toBN(2000),
  844. 3: web3.utils.toBN(3000),
  845. };
  846. beforeEach(async function () {
  847. this.token = await ERC1155.new(uri);
  848. await this.token.$_mintBatch(owner, Object.keys(tokenIds), Object.values(tokenIds), '0x');
  849. });
  850. it('can receive ERC1155 safeTransfer', async function () {
  851. await this.token.safeTransferFrom(
  852. owner,
  853. this.mock.address,
  854. ...Object.entries(tokenIds)[0], // id + amount
  855. '0x',
  856. { from: owner },
  857. );
  858. });
  859. it('can receive ERC1155 safeBatchTransfer', async function () {
  860. await this.token.safeBatchTransferFrom(
  861. owner,
  862. this.mock.address,
  863. Object.keys(tokenIds),
  864. Object.values(tokenIds),
  865. '0x',
  866. { from: owner },
  867. );
  868. });
  869. });
  870. });
  871. });
  872. }
  873. });