Governor.test.js 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995
  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', '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('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. });
  263. describe('on vote', function () {
  264. it('if proposal does not exist', async function () {
  265. await expectRevertCustomError(
  266. this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
  267. 'GovernorNonexistentProposal',
  268. [this.proposal.id],
  269. );
  270. });
  271. it('if voting has not started', async function () {
  272. await this.helper.propose();
  273. await expectRevertCustomError(
  274. this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
  275. 'GovernorUnexpectedProposalState',
  276. [this.proposal.id, Enums.ProposalState.Pending, proposalStatesToBitMap([Enums.ProposalState.Active])],
  277. );
  278. });
  279. it('if support value is invalid', async function () {
  280. await this.helper.propose();
  281. await this.helper.waitForSnapshot();
  282. await expectRevertCustomError(
  283. this.helper.vote({ support: web3.utils.toBN('255') }),
  284. 'GovernorInvalidVoteType',
  285. [],
  286. );
  287. });
  288. it('if vote was already casted', async function () {
  289. await this.helper.propose();
  290. await this.helper.waitForSnapshot();
  291. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  292. await expectRevertCustomError(
  293. this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
  294. 'GovernorAlreadyCastVote',
  295. [voter1],
  296. );
  297. });
  298. it('if voting is over', async function () {
  299. await this.helper.propose();
  300. await this.helper.waitForDeadline();
  301. await expectRevertCustomError(
  302. this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
  303. 'GovernorUnexpectedProposalState',
  304. [this.proposal.id, Enums.ProposalState.Defeated, proposalStatesToBitMap([Enums.ProposalState.Active])],
  305. );
  306. });
  307. });
  308. describe('on vote by signature', function () {
  309. beforeEach(async function () {
  310. this.voterBySig = Wallet.generate();
  311. this.voterBySig.address = web3.utils.toChecksumAddress(this.voterBySig.getAddressString());
  312. this.data = (contract, message) =>
  313. getDomain(contract).then(domain => ({
  314. primaryType: 'Ballot',
  315. types: {
  316. EIP712Domain: domainType(domain),
  317. Ballot: [
  318. { name: 'proposalId', type: 'uint256' },
  319. { name: 'support', type: 'uint8' },
  320. { name: 'voter', type: 'address' },
  321. { name: 'nonce', type: 'uint256' },
  322. ],
  323. },
  324. domain,
  325. message,
  326. }));
  327. this.signature = (contract, message) =>
  328. this.data(contract, message).then(data =>
  329. ethSigUtil.signTypedMessage(this.voterBySig.getPrivateKey(), { data }),
  330. );
  331. await this.token.delegate(this.voterBySig.address, { from: voter1 });
  332. // Run proposal
  333. await this.helper.propose();
  334. await this.helper.waitForSnapshot();
  335. });
  336. it('if signature does not match signer', async function () {
  337. const nonce = await this.mock.nonces(this.voterBySig.address);
  338. const voteParams = {
  339. support: Enums.VoteType.For,
  340. voter: this.voterBySig.address,
  341. nonce,
  342. signature: async (...params) => {
  343. const sig = await this.signature(...params);
  344. const tamperedSig = web3.utils.hexToBytes(sig);
  345. tamperedSig[42] ^= 0xff;
  346. return web3.utils.bytesToHex(tamperedSig);
  347. },
  348. };
  349. await expectRevertCustomError(this.helper.vote(voteParams), 'GovernorInvalidSignature', [voteParams.voter]);
  350. });
  351. it('if vote nonce is incorrect', async function () {
  352. const nonce = await this.mock.nonces(this.voterBySig.address);
  353. const voteParams = {
  354. support: Enums.VoteType.For,
  355. voter: this.voterBySig.address,
  356. nonce: nonce.addn(1),
  357. signature: this.signature,
  358. };
  359. await expectRevertCustomError(
  360. this.helper.vote(voteParams),
  361. // The signature check implies the nonce can't be tampered without changing the signer
  362. 'GovernorInvalidSignature',
  363. [voteParams.voter],
  364. );
  365. });
  366. });
  367. describe('on execute', function () {
  368. it('if proposal does not exist', async function () {
  369. await expectRevertCustomError(this.helper.execute(), 'GovernorNonexistentProposal', [this.proposal.id]);
  370. });
  371. it('if quorum is not reached', async function () {
  372. await this.helper.propose();
  373. await this.helper.waitForSnapshot();
  374. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter3 });
  375. await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [
  376. this.proposal.id,
  377. Enums.ProposalState.Active,
  378. proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]),
  379. ]);
  380. });
  381. it('if score not reached', async function () {
  382. await this.helper.propose();
  383. await this.helper.waitForSnapshot();
  384. await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter1 });
  385. await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [
  386. this.proposal.id,
  387. Enums.ProposalState.Active,
  388. proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]),
  389. ]);
  390. });
  391. it('if voting is not over', async function () {
  392. await this.helper.propose();
  393. await this.helper.waitForSnapshot();
  394. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  395. await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [
  396. this.proposal.id,
  397. Enums.ProposalState.Active,
  398. proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]),
  399. ]);
  400. });
  401. it('if receiver revert without reason', async function () {
  402. this.helper.setProposal(
  403. [
  404. {
  405. target: this.receiver.address,
  406. data: this.receiver.contract.methods.mockFunctionRevertsNoReason().encodeABI(),
  407. },
  408. ],
  409. '<proposal description>',
  410. );
  411. await this.helper.propose();
  412. await this.helper.waitForSnapshot();
  413. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  414. await this.helper.waitForDeadline();
  415. await expectRevertCustomError(this.helper.execute(), 'FailedInnerCall', []);
  416. });
  417. it('if receiver revert with reason', async function () {
  418. this.helper.setProposal(
  419. [
  420. {
  421. target: this.receiver.address,
  422. data: this.receiver.contract.methods.mockFunctionRevertsReason().encodeABI(),
  423. },
  424. ],
  425. '<proposal description>',
  426. );
  427. await this.helper.propose();
  428. await this.helper.waitForSnapshot();
  429. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  430. await this.helper.waitForDeadline();
  431. await expectRevert(this.helper.execute(), 'CallReceiverMock: reverting');
  432. });
  433. it('if proposal was already executed', 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. await this.helper.execute();
  439. await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [
  440. this.proposal.id,
  441. Enums.ProposalState.Executed,
  442. proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]),
  443. ]);
  444. });
  445. });
  446. });
  447. describe('state', function () {
  448. it('Unset', async function () {
  449. await expectRevertCustomError(this.mock.state(this.proposal.id), 'GovernorNonexistentProposal', [
  450. this.proposal.id,
  451. ]);
  452. });
  453. it('Pending & Active', async function () {
  454. await this.helper.propose();
  455. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending);
  456. await this.helper.waitForSnapshot();
  457. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending);
  458. await this.helper.waitForSnapshot(+1);
  459. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
  460. });
  461. it('Defeated', async function () {
  462. await this.helper.propose();
  463. await this.helper.waitForDeadline();
  464. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
  465. await this.helper.waitForDeadline(+1);
  466. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated);
  467. });
  468. it('Succeeded', async function () {
  469. await this.helper.propose();
  470. await this.helper.waitForSnapshot();
  471. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  472. await this.helper.waitForDeadline();
  473. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
  474. await this.helper.waitForDeadline(+1);
  475. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded);
  476. });
  477. it('Executed', async function () {
  478. await this.helper.propose();
  479. await this.helper.waitForSnapshot();
  480. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  481. await this.helper.waitForDeadline();
  482. await this.helper.execute();
  483. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Executed);
  484. });
  485. });
  486. describe('cancel', function () {
  487. describe('internal', function () {
  488. it('before proposal', async function () {
  489. await expectRevertCustomError(this.helper.cancel('internal'), 'GovernorNonexistentProposal', [
  490. this.proposal.id,
  491. ]);
  492. });
  493. it('after proposal', async function () {
  494. await this.helper.propose();
  495. await this.helper.cancel('internal');
  496. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
  497. await this.helper.waitForSnapshot();
  498. await expectRevertCustomError(
  499. this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
  500. 'GovernorUnexpectedProposalState',
  501. [this.proposal.id, Enums.ProposalState.Canceled, proposalStatesToBitMap([Enums.ProposalState.Active])],
  502. );
  503. });
  504. it('after vote', async function () {
  505. await this.helper.propose();
  506. await this.helper.waitForSnapshot();
  507. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  508. await this.helper.cancel('internal');
  509. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
  510. await this.helper.waitForDeadline();
  511. await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [
  512. this.proposal.id,
  513. Enums.ProposalState.Canceled,
  514. proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]),
  515. ]);
  516. });
  517. it('after deadline', async function () {
  518. await this.helper.propose();
  519. await this.helper.waitForSnapshot();
  520. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  521. await this.helper.waitForDeadline();
  522. await this.helper.cancel('internal');
  523. expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
  524. await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [
  525. this.proposal.id,
  526. Enums.ProposalState.Canceled,
  527. proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]),
  528. ]);
  529. });
  530. it('after execution', async function () {
  531. await this.helper.propose();
  532. await this.helper.waitForSnapshot();
  533. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  534. await this.helper.waitForDeadline();
  535. await this.helper.execute();
  536. await expectRevertCustomError(this.helper.cancel('internal'), 'GovernorUnexpectedProposalState', [
  537. this.proposal.id,
  538. Enums.ProposalState.Executed,
  539. proposalStatesToBitMap(
  540. [Enums.ProposalState.Canceled, Enums.ProposalState.Expired, Enums.ProposalState.Executed],
  541. { inverted: true },
  542. ),
  543. ]);
  544. });
  545. });
  546. describe('public', function () {
  547. it('before proposal', async function () {
  548. await expectRevertCustomError(this.helper.cancel('external'), 'GovernorNonexistentProposal', [
  549. this.proposal.id,
  550. ]);
  551. });
  552. it('after proposal', async function () {
  553. await this.helper.propose();
  554. await this.helper.cancel('external');
  555. });
  556. it('after proposal - restricted to proposer', async function () {
  557. await this.helper.propose();
  558. await expectRevertCustomError(this.helper.cancel('external', { from: owner }), 'GovernorOnlyProposer', [
  559. owner,
  560. ]);
  561. });
  562. it('after vote started', async function () {
  563. await this.helper.propose();
  564. await this.helper.waitForSnapshot(1); // snapshot + 1 block
  565. await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [
  566. this.proposal.id,
  567. Enums.ProposalState.Active,
  568. proposalStatesToBitMap([Enums.ProposalState.Pending]),
  569. ]);
  570. });
  571. it('after vote', async function () {
  572. await this.helper.propose();
  573. await this.helper.waitForSnapshot();
  574. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  575. await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [
  576. this.proposal.id,
  577. Enums.ProposalState.Active,
  578. proposalStatesToBitMap([Enums.ProposalState.Pending]),
  579. ]);
  580. });
  581. it('after deadline', async function () {
  582. await this.helper.propose();
  583. await this.helper.waitForSnapshot();
  584. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  585. await this.helper.waitForDeadline();
  586. await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [
  587. this.proposal.id,
  588. Enums.ProposalState.Succeeded,
  589. proposalStatesToBitMap([Enums.ProposalState.Pending]),
  590. ]);
  591. });
  592. it('after execution', async function () {
  593. await this.helper.propose();
  594. await this.helper.waitForSnapshot();
  595. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  596. await this.helper.waitForDeadline();
  597. await this.helper.execute();
  598. await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [
  599. this.proposal.id,
  600. Enums.ProposalState.Executed,
  601. proposalStatesToBitMap([Enums.ProposalState.Pending]),
  602. ]);
  603. });
  604. });
  605. });
  606. describe('proposal length', function () {
  607. it('empty', async function () {
  608. this.helper.setProposal([], '<proposal description>');
  609. await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [0, 0, 0]);
  610. });
  611. it('mismatch #1', async function () {
  612. this.helper.setProposal(
  613. {
  614. targets: [],
  615. values: [web3.utils.toWei('0')],
  616. data: [this.receiver.contract.methods.mockFunction().encodeABI()],
  617. },
  618. '<proposal description>',
  619. );
  620. await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [0, 1, 1]);
  621. });
  622. it('mismatch #2', async function () {
  623. this.helper.setProposal(
  624. {
  625. targets: [this.receiver.address],
  626. values: [],
  627. data: [this.receiver.contract.methods.mockFunction().encodeABI()],
  628. },
  629. '<proposal description>',
  630. );
  631. await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [1, 1, 0]);
  632. });
  633. it('mismatch #3', async function () {
  634. this.helper.setProposal(
  635. {
  636. targets: [this.receiver.address],
  637. values: [web3.utils.toWei('0')],
  638. data: [],
  639. },
  640. '<proposal description>',
  641. );
  642. await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [1, 0, 1]);
  643. });
  644. });
  645. describe('frontrun protection using description suffix', function () {
  646. describe('without protection', function () {
  647. describe('without suffix', function () {
  648. it('proposer can propose', async function () {
  649. expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated');
  650. });
  651. it('someone else can propose', async function () {
  652. expectEvent(await this.helper.propose({ from: voter1 }), 'ProposalCreated');
  653. });
  654. });
  655. describe('with different suffix', function () {
  656. beforeEach(async function () {
  657. this.proposal = this.helper.setProposal(
  658. [
  659. {
  660. target: this.receiver.address,
  661. data: this.receiver.contract.methods.mockFunction().encodeABI(),
  662. value,
  663. },
  664. ],
  665. `<proposal description>#wrong-suffix=${proposer}`,
  666. );
  667. });
  668. it('proposer can propose', async function () {
  669. expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated');
  670. });
  671. it('someone else can propose', async function () {
  672. expectEvent(await this.helper.propose({ from: voter1 }), 'ProposalCreated');
  673. });
  674. });
  675. describe('with proposer suffix but bad address part', function () {
  676. beforeEach(async function () {
  677. this.proposal = this.helper.setProposal(
  678. [
  679. {
  680. target: this.receiver.address,
  681. data: this.receiver.contract.methods.mockFunction().encodeABI(),
  682. value,
  683. },
  684. ],
  685. `<proposal description>#proposer=0x3C44CdDdB6a900fa2b585dd299e03d12FA429XYZ`, // XYZ are not a valid hex char
  686. );
  687. });
  688. it('propose can propose', async function () {
  689. expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated');
  690. });
  691. it('someone else can propose', async function () {
  692. expectEvent(await this.helper.propose({ from: voter1 }), 'ProposalCreated');
  693. });
  694. });
  695. });
  696. describe('with protection via proposer suffix', function () {
  697. beforeEach(async function () {
  698. this.proposal = this.helper.setProposal(
  699. [
  700. {
  701. target: this.receiver.address,
  702. data: this.receiver.contract.methods.mockFunction().encodeABI(),
  703. value,
  704. },
  705. ],
  706. `<proposal description>#proposer=${proposer}`,
  707. );
  708. });
  709. it('proposer can propose', async function () {
  710. expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated');
  711. });
  712. it('someone else cannot propose', async function () {
  713. await expectRevert(this.helper.propose({ from: voter1 }), 'Governor: proposer restricted');
  714. });
  715. });
  716. });
  717. describe('onlyGovernance updates', function () {
  718. it('setVotingDelay is protected', async function () {
  719. await expectRevertCustomError(this.mock.setVotingDelay('0', { from: owner }), 'GovernorOnlyExecutor', [
  720. owner,
  721. ]);
  722. });
  723. it('setVotingPeriod is protected', async function () {
  724. await expectRevertCustomError(this.mock.setVotingPeriod('32', { from: owner }), 'GovernorOnlyExecutor', [
  725. owner,
  726. ]);
  727. });
  728. it('setProposalThreshold is protected', async function () {
  729. await expectRevertCustomError(
  730. this.mock.setProposalThreshold('1000000000000000000', { from: owner }),
  731. 'GovernorOnlyExecutor',
  732. [owner],
  733. );
  734. });
  735. it('can setVotingDelay through governance', async function () {
  736. this.helper.setProposal(
  737. [
  738. {
  739. target: this.mock.address,
  740. data: this.mock.contract.methods.setVotingDelay('0').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. expectEvent(await this.helper.execute(), 'VotingDelaySet', { oldVotingDelay: '4', newVotingDelay: '0' });
  750. expect(await this.mock.votingDelay()).to.be.bignumber.equal('0');
  751. });
  752. it('can setVotingPeriod through governance', async function () {
  753. this.helper.setProposal(
  754. [
  755. {
  756. target: this.mock.address,
  757. data: this.mock.contract.methods.setVotingPeriod('32').encodeABI(),
  758. },
  759. ],
  760. '<proposal description>',
  761. );
  762. await this.helper.propose();
  763. await this.helper.waitForSnapshot();
  764. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  765. await this.helper.waitForDeadline();
  766. expectEvent(await this.helper.execute(), 'VotingPeriodSet', { oldVotingPeriod: '16', newVotingPeriod: '32' });
  767. expect(await this.mock.votingPeriod()).to.be.bignumber.equal('32');
  768. });
  769. it('cannot setVotingPeriod to 0 through governance', async function () {
  770. const votingPeriod = 0;
  771. this.helper.setProposal(
  772. [
  773. {
  774. target: this.mock.address,
  775. data: this.mock.contract.methods.setVotingPeriod(votingPeriod).encodeABI(),
  776. },
  777. ],
  778. '<proposal description>',
  779. );
  780. await this.helper.propose();
  781. await this.helper.waitForSnapshot();
  782. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  783. await this.helper.waitForDeadline();
  784. await expectRevertCustomError(this.helper.execute(), 'GovernorInvalidVotingPeriod', [votingPeriod]);
  785. });
  786. it('can setProposalThreshold to 0 through governance', async function () {
  787. this.helper.setProposal(
  788. [
  789. {
  790. target: this.mock.address,
  791. data: this.mock.contract.methods.setProposalThreshold('1000000000000000000').encodeABI(),
  792. },
  793. ],
  794. '<proposal description>',
  795. );
  796. await this.helper.propose();
  797. await this.helper.waitForSnapshot();
  798. await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
  799. await this.helper.waitForDeadline();
  800. expectEvent(await this.helper.execute(), 'ProposalThresholdSet', {
  801. oldProposalThreshold: '0',
  802. newProposalThreshold: '1000000000000000000',
  803. });
  804. expect(await this.mock.proposalThreshold()).to.be.bignumber.equal('1000000000000000000');
  805. });
  806. });
  807. describe('safe receive', function () {
  808. describe('ERC721', function () {
  809. const name = 'Non Fungible Token';
  810. const symbol = 'NFT';
  811. const tokenId = web3.utils.toBN(1);
  812. beforeEach(async function () {
  813. this.token = await ERC721.new(name, symbol);
  814. await this.token.$_mint(owner, tokenId);
  815. });
  816. it('can receive an ERC721 safeTransfer', async function () {
  817. await this.token.safeTransferFrom(owner, this.mock.address, tokenId, { from: owner });
  818. });
  819. });
  820. describe('ERC1155', function () {
  821. const uri = 'https://token-cdn-domain/{id}.json';
  822. const tokenIds = {
  823. 1: web3.utils.toBN(1000),
  824. 2: web3.utils.toBN(2000),
  825. 3: web3.utils.toBN(3000),
  826. };
  827. beforeEach(async function () {
  828. this.token = await ERC1155.new(uri);
  829. await this.token.$_mintBatch(owner, Object.keys(tokenIds), Object.values(tokenIds), '0x');
  830. });
  831. it('can receive ERC1155 safeTransfer', async function () {
  832. await this.token.safeTransferFrom(
  833. owner,
  834. this.mock.address,
  835. ...Object.entries(tokenIds)[0], // id + amount
  836. '0x',
  837. { from: owner },
  838. );
  839. });
  840. it('can receive ERC1155 safeBatchTransfer', async function () {
  841. await this.token.safeBatchTransferFrom(
  842. owner,
  843. this.mock.address,
  844. Object.keys(tokenIds),
  845. Object.values(tokenIds),
  846. '0x',
  847. { from: owner },
  848. );
  849. });
  850. });
  851. });
  852. });
  853. }
  854. });