Governor.test.js 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954
  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 { fromRpcSig, toRpcSig } = require('ethereumjs-util');
  6. const Enums = require('../helpers/enums');
  7. const { getDomain, domainType } = require('../helpers/eip712');
  8. const { GovernorHelper, proposalStatesToBitMap } = require('../helpers/governance');
  9. const { clockFromReceipt } = require('../helpers/time');
  10. const { expectRevertCustomError } = require('../helpers/customError');
  11. const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior');
  12. const { shouldBehaveLikeEIP6372 } = require('./utils/EIP6372.behavior');
  13. const { ZERO_BYTES32 } = require('@openzeppelin/test-helpers/src/constants');
  14. const Governor = artifacts.require('$GovernorMock');
  15. const CallReceiver = artifacts.require('CallReceiverMock');
  16. const ERC721 = artifacts.require('$ERC721');
  17. const ERC1155 = artifacts.require('$ERC1155');
  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', 'GovernorWithParams', 'GovernorCancel']);
  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('votes with signature', async function () {
  143. const voterBySig = Wallet.generate();
  144. const voterBySigAddress = web3.utils.toChecksumAddress(voterBySig.getAddressString());
  145. const signature = (contract, message) =>
  146. getDomain(contract)
  147. .then(domain => ({
  148. primaryType: 'Ballot',
  149. types: {
  150. EIP712Domain: domainType(domain),
  151. Ballot: [
  152. { name: 'proposalId', type: 'uint256' },
  153. { name: 'support', type: 'uint8' },
  154. { name: 'voter', type: 'address' },
  155. { name: 'nonce', type: 'uint256' },
  156. ],
  157. },
  158. domain,
  159. message,
  160. }))
  161. .then(data => ethSigUtil.signTypedMessage(voterBySig.getPrivateKey(), { data }))
  162. .then(fromRpcSig);
  163. await this.token.delegate(voterBySigAddress, { from: voter1 });
  164. const nonce = await this.mock.nonces(voterBySigAddress);
  165. // Run proposal
  166. await this.helper.propose();
  167. await this.helper.waitForSnapshot();
  168. expectEvent(
  169. await this.helper.vote({ support: Enums.VoteType.For, voter: voterBySigAddress, nonce, signature }),
  170. 'VoteCast',
  171. {
  172. voter: voterBySigAddress,
  173. support: Enums.VoteType.For,
  174. },
  175. );
  176. await this.helper.waitForDeadline();
  177. await this.helper.execute();
  178. // After
  179. expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
  180. expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false);
  181. expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false);
  182. expect(await this.mock.hasVoted(this.proposal.id, voterBySigAddress)).to.be.equal(true);
  183. expect(await this.mock.nonces(voterBySigAddress)).to.be.bignumber.equal(nonce.addn(1));
  184. });
  185. it('send ethers', async function () {
  186. const empty = web3.utils.toChecksumAddress(web3.utils.randomHex(20));
  187. this.proposal = this.helper.setProposal(
  188. [
  189. {
  190. target: empty,
  191. value,
  192. },
  193. ],
  194. '<proposal description>',
  195. );
  196. // Before
  197. expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value);
  198. expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal('0');
  199. // Run proposal
  200. await this.helper.propose();
  201. await this.helper.waitForSnapshot();
  202. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  203. await this.helper.waitForDeadline();
  204. await this.helper.execute();
  205. // After
  206. expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0');
  207. expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal(value);
  208. });
  209. describe('should revert', function () {
  210. describe('on propose', function () {
  211. it('if proposal already exists', async function () {
  212. await this.helper.propose();
  213. await expectRevertCustomError(this.helper.propose(), 'GovernorUnexpectedProposalState', [
  214. this.proposal.id,
  215. Enums.ProposalState.Pending,
  216. ZERO_BYTES32,
  217. ]);
  218. });
  219. });
  220. describe('on vote', function () {
  221. it('if proposal does not exist', async function () {
  222. await expectRevertCustomError(
  223. this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
  224. 'GovernorNonexistentProposal',
  225. [this.proposal.id],
  226. );
  227. });
  228. it('if voting has not started', async function () {
  229. await this.helper.propose();
  230. await expectRevertCustomError(
  231. this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
  232. 'GovernorUnexpectedProposalState',
  233. [this.proposal.id, Enums.ProposalState.Pending, proposalStatesToBitMap([Enums.ProposalState.Active])],
  234. );
  235. });
  236. it('if support value is invalid', async function () {
  237. await this.helper.propose();
  238. await this.helper.waitForSnapshot();
  239. await expectRevertCustomError(
  240. this.helper.vote({ support: web3.utils.toBN('255') }),
  241. 'GovernorInvalidVoteType',
  242. [],
  243. );
  244. });
  245. it('if vote was already casted', async function () {
  246. await this.helper.propose();
  247. await this.helper.waitForSnapshot();
  248. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  249. await expectRevertCustomError(
  250. this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
  251. 'GovernorAlreadyCastVote',
  252. [voter1],
  253. );
  254. });
  255. it('if voting is over', async function () {
  256. await this.helper.propose();
  257. await this.helper.waitForDeadline();
  258. await expectRevertCustomError(
  259. this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
  260. 'GovernorUnexpectedProposalState',
  261. [this.proposal.id, Enums.ProposalState.Defeated, proposalStatesToBitMap([Enums.ProposalState.Active])],
  262. );
  263. });
  264. });
  265. describe('on vote by signature', function () {
  266. beforeEach(async function () {
  267. this.voterBySig = Wallet.generate();
  268. this.voterBySig.address = web3.utils.toChecksumAddress(this.voterBySig.getAddressString());
  269. this.data = (contract, message) =>
  270. getDomain(contract).then(domain => ({
  271. primaryType: 'Ballot',
  272. types: {
  273. EIP712Domain: domainType(domain),
  274. Ballot: [
  275. { name: 'proposalId', type: 'uint256' },
  276. { name: 'support', type: 'uint8' },
  277. { name: 'voter', type: 'address' },
  278. { name: 'nonce', type: 'uint256' },
  279. ],
  280. },
  281. domain,
  282. message,
  283. }));
  284. this.signature = (contract, message) =>
  285. this.data(contract, message)
  286. .then(data => ethSigUtil.signTypedMessage(this.voterBySig.getPrivateKey(), { data }))
  287. .then(fromRpcSig);
  288. await this.token.delegate(this.voterBySig.address, { from: voter1 });
  289. // Run proposal
  290. await this.helper.propose();
  291. await this.helper.waitForSnapshot();
  292. });
  293. it('if signature does not match signer', async function () {
  294. const nonce = await this.mock.nonces(this.voterBySig.address);
  295. const voteParams = {
  296. support: Enums.VoteType.For,
  297. voter: this.voterBySig.address,
  298. nonce,
  299. signature: async (...params) => {
  300. const sig = await this.signature(...params);
  301. sig.s[12] ^= 0xff;
  302. return sig;
  303. },
  304. };
  305. const { r, s, v } = await this.helper.sign(voteParams);
  306. const message = this.helper.forgeMessage(voteParams);
  307. const data = await this.data(this.mock, message);
  308. await expectRevertCustomError(this.helper.vote(voteParams), 'GovernorInvalidSigner', [
  309. ethSigUtil.recoverTypedSignature({ sig: toRpcSig(v, r, s), data }),
  310. voteParams.voter,
  311. ]);
  312. });
  313. it('if vote nonce is incorrect', async function () {
  314. const nonce = await this.mock.nonces(this.voterBySig.address);
  315. const voteParams = {
  316. support: Enums.VoteType.For,
  317. voter: this.voterBySig.address,
  318. nonce: nonce.addn(1),
  319. signature: this.signature,
  320. };
  321. const { r, s, v } = await this.helper.sign(voteParams);
  322. const message = this.helper.forgeMessage(voteParams);
  323. const data = await this.data(this.mock, { ...message, nonce });
  324. await expectRevertCustomError(
  325. this.helper.vote(voteParams),
  326. // The signature check implies the nonce can't be tampered without changing the signer
  327. 'GovernorInvalidSigner',
  328. [ethSigUtil.recoverTypedSignature({ sig: toRpcSig(v, r, s), data }), voteParams.voter],
  329. );
  330. });
  331. });
  332. describe('on execute', function () {
  333. it('if proposal does not exist', async function () {
  334. await expectRevertCustomError(this.helper.execute(), 'GovernorNonexistentProposal', [this.proposal.id]);
  335. });
  336. it('if quorum is not reached', async function () {
  337. await this.helper.propose();
  338. await this.helper.waitForSnapshot();
  339. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter3 });
  340. await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [
  341. this.proposal.id,
  342. Enums.ProposalState.Active,
  343. proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]),
  344. ]);
  345. });
  346. it('if score not reached', async function () {
  347. await this.helper.propose();
  348. await this.helper.waitForSnapshot();
  349. await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter1 });
  350. await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [
  351. this.proposal.id,
  352. Enums.ProposalState.Active,
  353. proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]),
  354. ]);
  355. });
  356. it('if voting is not over', async function () {
  357. await this.helper.propose();
  358. await this.helper.waitForSnapshot();
  359. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  360. await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [
  361. this.proposal.id,
  362. Enums.ProposalState.Active,
  363. proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]),
  364. ]);
  365. });
  366. it('if receiver revert without reason', async function () {
  367. this.helper.setProposal(
  368. [
  369. {
  370. target: this.receiver.address,
  371. data: this.receiver.contract.methods.mockFunctionRevertsNoReason().encodeABI(),
  372. },
  373. ],
  374. '<proposal description>',
  375. );
  376. await this.helper.propose();
  377. await this.helper.waitForSnapshot();
  378. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  379. await this.helper.waitForDeadline();
  380. await expectRevertCustomError(this.helper.execute(), 'FailedInnerCall', []);
  381. });
  382. it('if receiver revert with reason', async function () {
  383. this.helper.setProposal(
  384. [
  385. {
  386. target: this.receiver.address,
  387. data: this.receiver.contract.methods.mockFunctionRevertsReason().encodeABI(),
  388. },
  389. ],
  390. '<proposal description>',
  391. );
  392. await this.helper.propose();
  393. await this.helper.waitForSnapshot();
  394. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  395. await this.helper.waitForDeadline();
  396. await expectRevert(this.helper.execute(), 'CallReceiverMock: reverting');
  397. });
  398. it('if proposal was already executed', async function () {
  399. await this.helper.propose();
  400. await this.helper.waitForSnapshot();
  401. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  402. await this.helper.waitForDeadline();
  403. await this.helper.execute();
  404. await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [
  405. this.proposal.id,
  406. Enums.ProposalState.Executed,
  407. proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]),
  408. ]);
  409. });
  410. });
  411. });
  412. describe('state', function () {
  413. it('Unset', async function () {
  414. await expectRevertCustomError(this.mock.state(this.proposal.id), 'GovernorNonexistentProposal', [
  415. this.proposal.id,
  416. ]);
  417. });
  418. it('Pending & Active', async function () {
  419. await this.helper.propose();
  420. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending);
  421. await this.helper.waitForSnapshot();
  422. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending);
  423. await this.helper.waitForSnapshot(+1);
  424. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
  425. });
  426. it('Defeated', async function () {
  427. await this.helper.propose();
  428. await this.helper.waitForDeadline();
  429. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
  430. await this.helper.waitForDeadline(+1);
  431. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated);
  432. });
  433. it('Succeeded', async function () {
  434. await this.helper.propose();
  435. await this.helper.waitForSnapshot();
  436. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  437. await this.helper.waitForDeadline();
  438. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
  439. await this.helper.waitForDeadline(+1);
  440. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded);
  441. });
  442. it('Executed', async function () {
  443. await this.helper.propose();
  444. await this.helper.waitForSnapshot();
  445. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  446. await this.helper.waitForDeadline();
  447. await this.helper.execute();
  448. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Executed);
  449. });
  450. });
  451. describe('cancel', function () {
  452. describe('internal', function () {
  453. it('before proposal', async function () {
  454. await expectRevertCustomError(this.helper.cancel('internal'), 'GovernorNonexistentProposal', [
  455. this.proposal.id,
  456. ]);
  457. });
  458. it('after proposal', async function () {
  459. await this.helper.propose();
  460. await this.helper.cancel('internal');
  461. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
  462. await this.helper.waitForSnapshot();
  463. await expectRevertCustomError(
  464. this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
  465. 'GovernorUnexpectedProposalState',
  466. [this.proposal.id, Enums.ProposalState.Canceled, proposalStatesToBitMap([Enums.ProposalState.Active])],
  467. );
  468. });
  469. it('after vote', async function () {
  470. await this.helper.propose();
  471. await this.helper.waitForSnapshot();
  472. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  473. await this.helper.cancel('internal');
  474. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
  475. await this.helper.waitForDeadline();
  476. await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [
  477. this.proposal.id,
  478. Enums.ProposalState.Canceled,
  479. proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]),
  480. ]);
  481. });
  482. it('after deadline', async function () {
  483. await this.helper.propose();
  484. await this.helper.waitForSnapshot();
  485. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  486. await this.helper.waitForDeadline();
  487. await this.helper.cancel('internal');
  488. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
  489. await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [
  490. this.proposal.id,
  491. Enums.ProposalState.Canceled,
  492. proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]),
  493. ]);
  494. });
  495. it('after execution', async function () {
  496. await this.helper.propose();
  497. await this.helper.waitForSnapshot();
  498. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  499. await this.helper.waitForDeadline();
  500. await this.helper.execute();
  501. await expectRevertCustomError(this.helper.cancel('internal'), 'GovernorUnexpectedProposalState', [
  502. this.proposal.id,
  503. Enums.ProposalState.Executed,
  504. proposalStatesToBitMap(
  505. [Enums.ProposalState.Canceled, Enums.ProposalState.Expired, Enums.ProposalState.Executed],
  506. { inverted: true },
  507. ),
  508. ]);
  509. });
  510. });
  511. describe('public', function () {
  512. it('before proposal', async function () {
  513. await expectRevertCustomError(this.helper.cancel('external'), 'GovernorNonexistentProposal', [
  514. this.proposal.id,
  515. ]);
  516. });
  517. it('after proposal', async function () {
  518. await this.helper.propose();
  519. await this.helper.cancel('external');
  520. });
  521. it('after proposal - restricted to proposer', async function () {
  522. await this.helper.propose();
  523. await expectRevertCustomError(this.helper.cancel('external', { from: owner }), 'GovernorOnlyProposer', [
  524. owner,
  525. ]);
  526. });
  527. it('after vote started', async function () {
  528. await this.helper.propose();
  529. await this.helper.waitForSnapshot(1); // snapshot + 1 block
  530. await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [
  531. this.proposal.id,
  532. Enums.ProposalState.Active,
  533. proposalStatesToBitMap([Enums.ProposalState.Pending]),
  534. ]);
  535. });
  536. it('after vote', 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 expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [
  541. this.proposal.id,
  542. Enums.ProposalState.Active,
  543. proposalStatesToBitMap([Enums.ProposalState.Pending]),
  544. ]);
  545. });
  546. it('after deadline', async function () {
  547. await this.helper.propose();
  548. await this.helper.waitForSnapshot();
  549. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  550. await this.helper.waitForDeadline();
  551. await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [
  552. this.proposal.id,
  553. Enums.ProposalState.Succeeded,
  554. proposalStatesToBitMap([Enums.ProposalState.Pending]),
  555. ]);
  556. });
  557. it('after execution', async function () {
  558. await this.helper.propose();
  559. await this.helper.waitForSnapshot();
  560. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  561. await this.helper.waitForDeadline();
  562. await this.helper.execute();
  563. await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [
  564. this.proposal.id,
  565. Enums.ProposalState.Executed,
  566. proposalStatesToBitMap([Enums.ProposalState.Pending]),
  567. ]);
  568. });
  569. });
  570. });
  571. describe('proposal length', function () {
  572. it('empty', async function () {
  573. this.helper.setProposal([], '<proposal description>');
  574. await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [0, 0, 0]);
  575. });
  576. it('mismatch #1', async function () {
  577. this.helper.setProposal(
  578. {
  579. targets: [],
  580. values: [web3.utils.toWei('0')],
  581. data: [this.receiver.contract.methods.mockFunction().encodeABI()],
  582. },
  583. '<proposal description>',
  584. );
  585. await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [0, 1, 1]);
  586. });
  587. it('mismatch #2', async function () {
  588. this.helper.setProposal(
  589. {
  590. targets: [this.receiver.address],
  591. values: [],
  592. data: [this.receiver.contract.methods.mockFunction().encodeABI()],
  593. },
  594. '<proposal description>',
  595. );
  596. await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [1, 1, 0]);
  597. });
  598. it('mismatch #3', async function () {
  599. this.helper.setProposal(
  600. {
  601. targets: [this.receiver.address],
  602. values: [web3.utils.toWei('0')],
  603. data: [],
  604. },
  605. '<proposal description>',
  606. );
  607. await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [1, 0, 1]);
  608. });
  609. });
  610. describe('frontrun protection using description suffix', function () {
  611. describe('without protection', function () {
  612. describe('without suffix', function () {
  613. it('proposer can propose', async function () {
  614. expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated');
  615. });
  616. it('someone else can propose', async function () {
  617. expectEvent(await this.helper.propose({ from: voter1 }), 'ProposalCreated');
  618. });
  619. });
  620. describe('with different suffix', function () {
  621. beforeEach(async function () {
  622. this.proposal = this.helper.setProposal(
  623. [
  624. {
  625. target: this.receiver.address,
  626. data: this.receiver.contract.methods.mockFunction().encodeABI(),
  627. value,
  628. },
  629. ],
  630. `<proposal description>#wrong-suffix=${proposer}`,
  631. );
  632. });
  633. it('proposer can propose', async function () {
  634. expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated');
  635. });
  636. it('someone else can propose', async function () {
  637. expectEvent(await this.helper.propose({ from: voter1 }), 'ProposalCreated');
  638. });
  639. });
  640. describe('with proposer suffix but bad address part', function () {
  641. beforeEach(async function () {
  642. this.proposal = this.helper.setProposal(
  643. [
  644. {
  645. target: this.receiver.address,
  646. data: this.receiver.contract.methods.mockFunction().encodeABI(),
  647. value,
  648. },
  649. ],
  650. `<proposal description>#proposer=0x3C44CdDdB6a900fa2b585dd299e03d12FA429XYZ`, // XYZ are not a valid hex char
  651. );
  652. });
  653. it('propose can propose', async function () {
  654. expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated');
  655. });
  656. it('someone else can propose', async function () {
  657. expectEvent(await this.helper.propose({ from: voter1 }), 'ProposalCreated');
  658. });
  659. });
  660. });
  661. describe('with protection via proposer suffix', function () {
  662. beforeEach(async function () {
  663. this.proposal = this.helper.setProposal(
  664. [
  665. {
  666. target: this.receiver.address,
  667. data: this.receiver.contract.methods.mockFunction().encodeABI(),
  668. value,
  669. },
  670. ],
  671. `<proposal description>#proposer=${proposer}`,
  672. );
  673. });
  674. it('proposer can propose', async function () {
  675. expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated');
  676. });
  677. it('someone else cannot propose', async function () {
  678. await expectRevert(this.helper.propose({ from: voter1 }), 'Governor: proposer restricted');
  679. });
  680. });
  681. });
  682. describe('onlyGovernance updates', function () {
  683. it('setVotingDelay is protected', async function () {
  684. await expectRevertCustomError(this.mock.setVotingDelay('0', { from: owner }), 'GovernorOnlyExecutor', [
  685. owner,
  686. ]);
  687. });
  688. it('setVotingPeriod is protected', async function () {
  689. await expectRevertCustomError(this.mock.setVotingPeriod('32', { from: owner }), 'GovernorOnlyExecutor', [
  690. owner,
  691. ]);
  692. });
  693. it('setProposalThreshold is protected', async function () {
  694. await expectRevertCustomError(
  695. this.mock.setProposalThreshold('1000000000000000000', { from: owner }),
  696. 'GovernorOnlyExecutor',
  697. [owner],
  698. );
  699. });
  700. it('can setVotingDelay through governance', async function () {
  701. this.helper.setProposal(
  702. [
  703. {
  704. target: this.mock.address,
  705. data: this.mock.contract.methods.setVotingDelay('0').encodeABI(),
  706. },
  707. ],
  708. '<proposal description>',
  709. );
  710. await this.helper.propose();
  711. await this.helper.waitForSnapshot();
  712. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  713. await this.helper.waitForDeadline();
  714. expectEvent(await this.helper.execute(), 'VotingDelaySet', { oldVotingDelay: '4', newVotingDelay: '0' });
  715. expect(await this.mock.votingDelay()).to.be.bignumber.equal('0');
  716. });
  717. it('can setVotingPeriod through governance', async function () {
  718. this.helper.setProposal(
  719. [
  720. {
  721. target: this.mock.address,
  722. data: this.mock.contract.methods.setVotingPeriod('32').encodeABI(),
  723. },
  724. ],
  725. '<proposal description>',
  726. );
  727. await this.helper.propose();
  728. await this.helper.waitForSnapshot();
  729. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  730. await this.helper.waitForDeadline();
  731. expectEvent(await this.helper.execute(), 'VotingPeriodSet', { oldVotingPeriod: '16', newVotingPeriod: '32' });
  732. expect(await this.mock.votingPeriod()).to.be.bignumber.equal('32');
  733. });
  734. it('cannot setVotingPeriod to 0 through governance', async function () {
  735. const votingPeriod = 0;
  736. this.helper.setProposal(
  737. [
  738. {
  739. target: this.mock.address,
  740. data: this.mock.contract.methods.setVotingPeriod(votingPeriod).encodeABI(),
  741. },
  742. ],
  743. '<proposal description>',
  744. );
  745. await this.helper.propose();
  746. await this.helper.waitForSnapshot();
  747. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  748. await this.helper.waitForDeadline();
  749. await expectRevertCustomError(this.helper.execute(), 'GovernorInvalidVotingPeriod', [votingPeriod]);
  750. });
  751. it('can setProposalThreshold to 0 through governance', async function () {
  752. this.helper.setProposal(
  753. [
  754. {
  755. target: this.mock.address,
  756. data: this.mock.contract.methods.setProposalThreshold('1000000000000000000').encodeABI(),
  757. },
  758. ],
  759. '<proposal description>',
  760. );
  761. await this.helper.propose();
  762. await this.helper.waitForSnapshot();
  763. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  764. await this.helper.waitForDeadline();
  765. expectEvent(await this.helper.execute(), 'ProposalThresholdSet', {
  766. oldProposalThreshold: '0',
  767. newProposalThreshold: '1000000000000000000',
  768. });
  769. expect(await this.mock.proposalThreshold()).to.be.bignumber.equal('1000000000000000000');
  770. });
  771. });
  772. describe('safe receive', function () {
  773. describe('ERC721', function () {
  774. const name = 'Non Fungible Token';
  775. const symbol = 'NFT';
  776. const tokenId = web3.utils.toBN(1);
  777. beforeEach(async function () {
  778. this.token = await ERC721.new(name, symbol);
  779. await this.token.$_mint(owner, tokenId);
  780. });
  781. it('can receive an ERC721 safeTransfer', async function () {
  782. await this.token.safeTransferFrom(owner, this.mock.address, tokenId, { from: owner });
  783. });
  784. });
  785. describe('ERC1155', function () {
  786. const uri = 'https://token-cdn-domain/{id}.json';
  787. const tokenIds = {
  788. 1: web3.utils.toBN(1000),
  789. 2: web3.utils.toBN(2000),
  790. 3: web3.utils.toBN(3000),
  791. };
  792. beforeEach(async function () {
  793. this.token = await ERC1155.new(uri);
  794. await this.token.$_mintBatch(owner, Object.keys(tokenIds), Object.values(tokenIds), '0x');
  795. });
  796. it('can receive ERC1155 safeTransfer', async function () {
  797. await this.token.safeTransferFrom(
  798. owner,
  799. this.mock.address,
  800. ...Object.entries(tokenIds)[0], // id + amount
  801. '0x',
  802. { from: owner },
  803. );
  804. });
  805. it('can receive ERC1155 safeBatchTransfer', async function () {
  806. await this.token.safeBatchTransferFrom(
  807. owner,
  808. this.mock.address,
  809. Object.keys(tokenIds),
  810. Object.values(tokenIds),
  811. '0x',
  812. { from: owner },
  813. );
  814. });
  815. });
  816. });
  817. });
  818. }
  819. });