GovernorCompatibilityBravo.test.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. const { BN, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
  2. const Enums = require('../../helpers/enums');
  3. const RLP = require('rlp');
  4. const {
  5. runGovernorWorkflow,
  6. } = require('../GovernorWorkflow.behavior');
  7. const Token = artifacts.require('ERC20VotesCompMock');
  8. const Timelock = artifacts.require('CompTimelock');
  9. const Governor = artifacts.require('GovernorCompatibilityBravoMock');
  10. const CallReceiver = artifacts.require('CallReceiverMock');
  11. async function getReceiptOrRevert (promise, error = undefined) {
  12. if (error) {
  13. await expectRevert(promise, error);
  14. return undefined;
  15. } else {
  16. const { receipt } = await promise;
  17. return receipt;
  18. }
  19. }
  20. function tryGet (obj, path = '') {
  21. try {
  22. return path.split('.').reduce((o, k) => o[k], obj);
  23. } catch (_) {
  24. return undefined;
  25. }
  26. }
  27. function makeContractAddress (creator, nonce) {
  28. return web3.utils.toChecksumAddress(web3.utils.sha3(RLP.encode([creator, nonce])).slice(12).substring(14));
  29. }
  30. contract('GovernorCompatibilityBravo', function (accounts) {
  31. const [ owner, proposer, voter1, voter2, voter3, voter4, other ] = accounts;
  32. const name = 'OZ-Governor';
  33. // const version = '1';
  34. const tokenName = 'MockToken';
  35. const tokenSymbol = 'MTKN';
  36. const tokenSupply = web3.utils.toWei('100');
  37. const proposalThreshold = web3.utils.toWei('10');
  38. beforeEach(async function () {
  39. const [ deployer ] = await web3.eth.getAccounts();
  40. this.token = await Token.new(tokenName, tokenSymbol);
  41. // Need to predict governance address to set it as timelock admin with a delayed transfer
  42. const nonce = await web3.eth.getTransactionCount(deployer);
  43. const predictGovernor = makeContractAddress(deployer, nonce + 1);
  44. this.timelock = await Timelock.new(predictGovernor, 2 * 86400);
  45. this.mock = await Governor.new(name, this.token.address, 4, 16, proposalThreshold, this.timelock.address);
  46. this.receiver = await CallReceiver.new();
  47. await this.token.mint(owner, tokenSupply);
  48. await this.token.delegate(voter1, { from: voter1 });
  49. await this.token.delegate(voter2, { from: voter2 });
  50. await this.token.delegate(voter3, { from: voter3 });
  51. await this.token.delegate(voter4, { from: voter4 });
  52. await this.token.transfer(proposer, proposalThreshold, { from: owner });
  53. await this.token.delegate(proposer, { from: proposer });
  54. });
  55. it('deployment check', async function () {
  56. expect(await this.mock.name()).to.be.equal(name);
  57. expect(await this.mock.token()).to.be.equal(this.token.address);
  58. expect(await this.mock.votingDelay()).to.be.bignumber.equal('4');
  59. expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16');
  60. expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
  61. expect(await this.mock.quorumVotes()).to.be.bignumber.equal('0');
  62. expect(await this.mock.COUNTING_MODE()).to.be.equal('support=bravo&quorum=bravo');
  63. });
  64. describe('nominal', function () {
  65. beforeEach(async function () {
  66. this.settings = {
  67. proposal: [
  68. [ this.receiver.address ], // targets
  69. [ web3.utils.toWei('0') ], // values
  70. [ this.receiver.contract.methods.mockFunction().encodeABI() ], // calldatas
  71. '<proposal description>', // description
  72. ],
  73. proposer,
  74. tokenHolder: owner,
  75. voters: [
  76. {
  77. voter: voter1,
  78. weight: web3.utils.toWei('1'),
  79. support: Enums.VoteType.Abstain,
  80. },
  81. {
  82. voter: voter2,
  83. weight: web3.utils.toWei('10'),
  84. support: Enums.VoteType.For,
  85. },
  86. {
  87. voter: voter3,
  88. weight: web3.utils.toWei('5'),
  89. support: Enums.VoteType.Against,
  90. },
  91. {
  92. voter: voter4,
  93. support: '100',
  94. error: 'GovernorCompatibilityBravo: invalid vote type',
  95. },
  96. {
  97. voter: voter1,
  98. support: Enums.VoteType.For,
  99. error: 'GovernorCompatibilityBravo: vote already casted',
  100. skip: true,
  101. },
  102. ],
  103. steps: {
  104. queue: { delay: 7 * 86400 },
  105. },
  106. };
  107. this.votingDelay = await this.mock.votingDelay();
  108. this.votingPeriod = await this.mock.votingPeriod();
  109. this.receipts = {};
  110. });
  111. afterEach(async function () {
  112. const proposal = await this.mock.proposals(this.id);
  113. expect(proposal.id).to.be.bignumber.equal(this.id);
  114. expect(proposal.proposer).to.be.equal(proposer);
  115. expect(proposal.eta).to.be.bignumber.equal(this.eta);
  116. expect(proposal.startBlock).to.be.bignumber.equal(this.snapshot);
  117. expect(proposal.endBlock).to.be.bignumber.equal(this.deadline);
  118. expect(proposal.canceled).to.be.equal(false);
  119. expect(proposal.executed).to.be.equal(true);
  120. for (const [key, value] of Object.entries(Enums.VoteType)) {
  121. expect(proposal[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal(
  122. Object.values(this.settings.voters).filter(({ support }) => support === value).reduce(
  123. (acc, { weight }) => acc.add(new BN(weight)),
  124. new BN('0'),
  125. ),
  126. );
  127. }
  128. const action = await this.mock.getActions(this.id);
  129. expect(action.targets).to.be.deep.equal(this.settings.proposal[0]);
  130. // expect(action.values).to.be.deep.equal(this.settings.proposal[1]);
  131. expect(action.signatures).to.be.deep.equal(Array(this.settings.proposal[2].length).fill(''));
  132. expect(action.calldatas).to.be.deep.equal(this.settings.proposal[2]);
  133. for (const voter of this.settings.voters.filter(({ skip }) => !skip)) {
  134. expect(await this.mock.hasVoted(this.id, voter.voter)).to.be.equal(voter.error === undefined);
  135. const receipt = await this.mock.getReceipt(this.id, voter.voter);
  136. expect(receipt.hasVoted).to.be.equal(voter.error === undefined);
  137. expect(receipt.support).to.be.bignumber.equal(voter.error === undefined ? voter.support : '0');
  138. expect(receipt.votes).to.be.bignumber.equal(voter.error === undefined ? voter.weight : '0');
  139. }
  140. expectEvent(
  141. this.receipts.propose,
  142. 'ProposalCreated',
  143. {
  144. proposalId: this.id,
  145. proposer,
  146. targets: this.settings.proposal[0],
  147. // values: this.settings.proposal[1].map(value => new BN(value)),
  148. signatures: this.settings.proposal[2].map(() => ''),
  149. calldatas: this.settings.proposal[2],
  150. startBlock: new BN(this.receipts.propose.blockNumber).add(this.votingDelay),
  151. endBlock: new BN(this.receipts.propose.blockNumber).add(this.votingDelay).add(this.votingPeriod),
  152. description: this.settings.proposal[3],
  153. },
  154. );
  155. this.receipts.castVote.filter(Boolean).forEach(vote => {
  156. const { voter } = vote.logs.find(Boolean).args;
  157. expectEvent(
  158. vote,
  159. 'VoteCast',
  160. this.settings.voters.find(({ address }) => address === voter),
  161. );
  162. });
  163. expectEvent(
  164. this.receipts.execute,
  165. 'ProposalExecuted',
  166. { proposalId: this.id },
  167. );
  168. await expectEvent.inTransaction(
  169. this.receipts.execute.transactionHash,
  170. this.receiver,
  171. 'MockFunctionCalled',
  172. );
  173. });
  174. runGovernorWorkflow();
  175. });
  176. describe('proposalThreshold not reached', function () {
  177. beforeEach(async function () {
  178. this.settings = {
  179. proposal: [
  180. [ this.receiver.address ], // targets
  181. [ web3.utils.toWei('0') ], // values
  182. [ this.receiver.contract.methods.mockFunction().encodeABI() ], // calldatas
  183. '<proposal description>', // description
  184. ],
  185. proposer: other,
  186. steps: {
  187. propose: { error: 'GovernorCompatibilityBravo: proposer votes below proposal threshold' },
  188. wait: { enable: false },
  189. queue: { enable: false },
  190. execute: { enable: false },
  191. },
  192. };
  193. });
  194. runGovernorWorkflow();
  195. });
  196. describe('with compatibility interface', function () {
  197. beforeEach(async function () {
  198. this.settings = {
  199. proposal: [
  200. [ this.receiver.address ], // targets
  201. [ web3.utils.toWei('0') ], // values
  202. [ '' ], // signatures
  203. [ this.receiver.contract.methods.mockFunction().encodeABI() ], // calldatas
  204. '<proposal description>', // description
  205. ],
  206. proposer,
  207. tokenHolder: owner,
  208. voters: [
  209. {
  210. voter: voter1,
  211. weight: web3.utils.toWei('1'),
  212. support: Enums.VoteType.Abstain,
  213. },
  214. {
  215. voter: voter2,
  216. weight: web3.utils.toWei('10'),
  217. support: Enums.VoteType.For,
  218. },
  219. {
  220. voter: voter3,
  221. weight: web3.utils.toWei('5'),
  222. support: Enums.VoteType.Against,
  223. },
  224. {
  225. voter: voter4,
  226. support: '100',
  227. error: 'GovernorCompatibilityBravo: invalid vote type',
  228. },
  229. {
  230. voter: voter1,
  231. support: Enums.VoteType.For,
  232. error: 'GovernorCompatibilityBravo: vote already casted',
  233. skip: true,
  234. },
  235. ],
  236. steps: {
  237. queue: { delay: 7 * 86400 },
  238. },
  239. };
  240. this.votingDelay = await this.mock.votingDelay();
  241. this.votingPeriod = await this.mock.votingPeriod();
  242. this.receipts = {};
  243. });
  244. afterEach(async function () {
  245. const proposal = await this.mock.proposals(this.id);
  246. expect(proposal.id).to.be.bignumber.equal(this.id);
  247. expect(proposal.proposer).to.be.equal(proposer);
  248. expect(proposal.eta).to.be.bignumber.equal(this.eta);
  249. expect(proposal.startBlock).to.be.bignumber.equal(this.snapshot);
  250. expect(proposal.endBlock).to.be.bignumber.equal(this.deadline);
  251. expect(proposal.canceled).to.be.equal(false);
  252. expect(proposal.executed).to.be.equal(true);
  253. for (const [key, value] of Object.entries(Enums.VoteType)) {
  254. expect(proposal[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal(
  255. Object.values(this.settings.voters).filter(({ support }) => support === value).reduce(
  256. (acc, { weight }) => acc.add(new BN(weight)),
  257. new BN('0'),
  258. ),
  259. );
  260. }
  261. const action = await this.mock.getActions(this.id);
  262. expect(action.targets).to.be.deep.equal(this.settings.proposal[0]);
  263. // expect(action.values).to.be.deep.equal(this.settings.proposal[1]);
  264. expect(action.signatures).to.be.deep.equal(this.settings.proposal[2]);
  265. expect(action.calldatas).to.be.deep.equal(this.settings.proposal[3]);
  266. for (const voter of this.settings.voters.filter(({ skip }) => !skip)) {
  267. expect(await this.mock.hasVoted(this.id, voter.voter)).to.be.equal(voter.error === undefined);
  268. const receipt = await this.mock.getReceipt(this.id, voter.voter);
  269. expect(receipt.hasVoted).to.be.equal(voter.error === undefined);
  270. expect(receipt.support).to.be.bignumber.equal(voter.error === undefined ? voter.support : '0');
  271. expect(receipt.votes).to.be.bignumber.equal(voter.error === undefined ? voter.weight : '0');
  272. }
  273. expectEvent(
  274. this.receipts.propose,
  275. 'ProposalCreated',
  276. {
  277. proposalId: this.id,
  278. proposer,
  279. targets: this.settings.proposal[0],
  280. // values: this.settings.proposal[1].map(value => new BN(value)),
  281. signatures: this.settings.proposal[2],
  282. calldatas: this.settings.proposal[3],
  283. startBlock: new BN(this.receipts.propose.blockNumber).add(this.votingDelay),
  284. endBlock: new BN(this.receipts.propose.blockNumber).add(this.votingDelay).add(this.votingPeriod),
  285. description: this.settings.proposal[4],
  286. },
  287. );
  288. this.receipts.castVote.filter(Boolean).forEach(vote => {
  289. const { voter } = vote.logs.find(Boolean).args;
  290. expectEvent(
  291. vote,
  292. 'VoteCast',
  293. this.settings.voters.find(({ address }) => address === voter),
  294. );
  295. });
  296. expectEvent(
  297. this.receipts.execute,
  298. 'ProposalExecuted',
  299. { proposalId: this.id },
  300. );
  301. await expectEvent.inTransaction(
  302. this.receipts.execute.transactionHash,
  303. this.receiver,
  304. 'MockFunctionCalled',
  305. );
  306. });
  307. it('run', async function () {
  308. // transfer tokens
  309. if (tryGet(this.settings, 'voters')) {
  310. for (const voter of this.settings.voters) {
  311. if (voter.weight) {
  312. await this.token.transfer(voter.voter, voter.weight, { from: this.settings.tokenHolder });
  313. }
  314. }
  315. }
  316. // propose
  317. if (this.mock.propose && tryGet(this.settings, 'steps.propose.enable') !== false) {
  318. this.receipts.propose = await getReceiptOrRevert(
  319. this.mock.methods['propose(address[],uint256[],string[],bytes[],string)'](
  320. ...this.settings.proposal,
  321. { from: this.settings.proposer },
  322. ),
  323. tryGet(this.settings, 'steps.propose.error'),
  324. );
  325. if (tryGet(this.settings, 'steps.propose.error') === undefined) {
  326. this.id = this.receipts.propose.logs.find(({ event }) => event === 'ProposalCreated').args.proposalId;
  327. this.snapshot = await this.mock.proposalSnapshot(this.id);
  328. this.deadline = await this.mock.proposalDeadline(this.id);
  329. }
  330. if (tryGet(this.settings, 'steps.propose.delay')) {
  331. await time.increase(tryGet(this.settings, 'steps.propose.delay'));
  332. }
  333. if (
  334. tryGet(this.settings, 'steps.propose.error') === undefined &&
  335. tryGet(this.settings, 'steps.propose.noadvance') !== true
  336. ) {
  337. await time.advanceBlockTo(this.snapshot);
  338. }
  339. }
  340. // vote
  341. if (tryGet(this.settings, 'voters')) {
  342. this.receipts.castVote = [];
  343. for (const voter of this.settings.voters) {
  344. if (!voter.signature) {
  345. this.receipts.castVote.push(
  346. await getReceiptOrRevert(
  347. this.mock.castVote(this.id, voter.support, { from: voter.voter }),
  348. voter.error,
  349. ),
  350. );
  351. } else {
  352. const { v, r, s } = await voter.signature({ proposalId: this.id, support: voter.support });
  353. this.receipts.castVote.push(
  354. await getReceiptOrRevert(
  355. this.mock.castVoteBySig(this.id, voter.support, v, r, s),
  356. voter.error,
  357. ),
  358. );
  359. }
  360. if (tryGet(voter, 'delay')) {
  361. await time.increase(tryGet(voter, 'delay'));
  362. }
  363. }
  364. }
  365. // fast forward
  366. if (tryGet(this.settings, 'steps.wait.enable') !== false) {
  367. await time.advanceBlockTo(this.deadline);
  368. }
  369. // queue
  370. if (this.mock.queue && tryGet(this.settings, 'steps.queue.enable') !== false) {
  371. this.receipts.queue = await getReceiptOrRevert(
  372. this.mock.methods['queue(uint256)'](this.id, { from: this.settings.queuer }),
  373. tryGet(this.settings, 'steps.queue.error'),
  374. );
  375. this.eta = await this.mock.proposalEta(this.id);
  376. if (tryGet(this.settings, 'steps.queue.delay')) {
  377. await time.increase(tryGet(this.settings, 'steps.queue.delay'));
  378. }
  379. }
  380. // execute
  381. if (this.mock.execute && tryGet(this.settings, 'steps.execute.enable') !== false) {
  382. this.receipts.execute = await getReceiptOrRevert(
  383. this.mock.methods['execute(uint256)'](this.id, { from: this.settings.executer }),
  384. tryGet(this.settings, 'steps.execute.error'),
  385. );
  386. if (tryGet(this.settings, 'steps.execute.delay')) {
  387. await time.increase(tryGet(this.settings, 'steps.execute.delay'));
  388. }
  389. }
  390. });
  391. });
  392. });