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