governance.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. const { ethers } = require('hardhat');
  2. const { ProposalState } = require('./enums');
  3. const { unique } = require('./iterate');
  4. const time = require('./time');
  5. const timelockSalt = (address, descriptionHash) =>
  6. ethers.toBeHex((ethers.toBigInt(address) << 96n) ^ ethers.toBigInt(descriptionHash), 32);
  7. class GovernorHelper {
  8. constructor(governor, mode = 'blocknumber') {
  9. this.governor = governor;
  10. this.mode = mode;
  11. }
  12. connect(account) {
  13. this.governor = this.governor.connect(account);
  14. return this;
  15. }
  16. /// Setter and getters
  17. /**
  18. * Specify a proposal either as
  19. * 1) an array of objects [{ target, value, data }]
  20. * 2) an object of arrays { targets: [], values: [], data: [] }
  21. */
  22. setProposal(actions, description) {
  23. if (Array.isArray(actions)) {
  24. this.targets = actions.map(a => a.target);
  25. this.values = actions.map(a => a.value || 0n);
  26. this.data = actions.map(a => a.data || '0x');
  27. } else {
  28. ({ targets: this.targets, values: this.values, data: this.data } = actions);
  29. }
  30. this.description = description;
  31. return this;
  32. }
  33. get id() {
  34. return ethers.keccak256(
  35. ethers.AbiCoder.defaultAbiCoder().encode(['address[]', 'uint256[]', 'bytes[]', 'bytes32'], this.shortProposal),
  36. );
  37. }
  38. // used for checking events
  39. get signatures() {
  40. return this.data.map(() => '');
  41. }
  42. get descriptionHash() {
  43. return ethers.id(this.description);
  44. }
  45. // condensed version for queueing end executing
  46. get shortProposal() {
  47. return [this.targets, this.values, this.data, this.descriptionHash];
  48. }
  49. // full version for proposing
  50. get fullProposal() {
  51. return [this.targets, this.values, this.data, this.description];
  52. }
  53. get currentProposal() {
  54. return this;
  55. }
  56. /// Proposal lifecycle
  57. delegate(delegation) {
  58. return Promise.all([
  59. delegation.token.connect(delegation.to).delegate(delegation.to),
  60. delegation.value === undefined ||
  61. delegation.token.connect(this.governor.runner).transfer(delegation.to, delegation.value),
  62. delegation.tokenId === undefined ||
  63. delegation.token
  64. .ownerOf(delegation.tokenId)
  65. .then(owner =>
  66. delegation.token.connect(this.governor.runner).transferFrom(owner, delegation.to, delegation.tokenId),
  67. ),
  68. ]);
  69. }
  70. propose() {
  71. return this.governor.propose(...this.fullProposal);
  72. }
  73. queue() {
  74. return this.governor.queue(...this.shortProposal);
  75. }
  76. execute() {
  77. return this.governor.execute(...this.shortProposal);
  78. }
  79. cancel(visibility = 'external') {
  80. switch (visibility) {
  81. case 'external':
  82. return this.governor.cancel(...this.shortProposal);
  83. case 'internal':
  84. return this.governor.$_cancel(...this.shortProposal);
  85. default:
  86. throw new Error(`unsupported visibility "${visibility}"`);
  87. }
  88. }
  89. async vote(vote = {}) {
  90. let method = 'castVote'; // default
  91. let args = [this.id, vote.support]; // base
  92. if (vote.signature) {
  93. const sign = await vote.signature(this.governor, this.forgeMessage(vote));
  94. if (vote.params || vote.reason) {
  95. method = 'castVoteWithReasonAndParamsBySig';
  96. args.push(vote.voter, vote.reason ?? '', vote.params ?? '0x', sign);
  97. } else {
  98. method = 'castVoteBySig';
  99. args.push(vote.voter, sign);
  100. }
  101. } else if (vote.params) {
  102. method = 'castVoteWithReasonAndParams';
  103. args.push(vote.reason ?? '', vote.params);
  104. } else if (vote.reason) {
  105. method = 'castVoteWithReason';
  106. args.push(vote.reason);
  107. }
  108. return await this.governor[method](...args);
  109. }
  110. /// Clock helpers
  111. async waitForSnapshot(offset = 0n) {
  112. const timepoint = await this.governor.proposalSnapshot(this.id);
  113. return time.increaseTo[this.mode](timepoint + offset);
  114. }
  115. async waitForDeadline(offset = 0n) {
  116. const timepoint = await this.governor.proposalDeadline(this.id);
  117. return time.increaseTo[this.mode](timepoint + offset);
  118. }
  119. async waitForEta(offset = 0n) {
  120. const timestamp = await this.governor.proposalEta(this.id);
  121. return time.increaseTo.timestamp(timestamp + offset);
  122. }
  123. /// Other helpers
  124. forgeMessage(vote = {}) {
  125. const message = { proposalId: this.id, support: vote.support, voter: vote.voter, nonce: vote.nonce };
  126. if (vote.params || vote.reason) {
  127. message.reason = vote.reason ?? '';
  128. message.params = vote.params ?? '0x';
  129. }
  130. return message;
  131. }
  132. /**
  133. * Encodes a list ProposalStates into a bytes32 representation where each bit enabled corresponds to
  134. * the underlying position in the `ProposalState` enum. For example:
  135. *
  136. * 0x000...10000
  137. * ^^^^^^------ ...
  138. * ^----- Succeeded
  139. * ^---- Defeated
  140. * ^--- Canceled
  141. * ^-- Active
  142. * ^- Pending
  143. */
  144. static proposalStatesToBitMap(proposalStates, options = {}) {
  145. if (!Array.isArray(proposalStates)) {
  146. proposalStates = [proposalStates];
  147. }
  148. const statesCount = ethers.toBigInt(Object.keys(ProposalState).length);
  149. let result = 0n;
  150. for (const state of unique(proposalStates)) {
  151. if (state < 0n || state >= statesCount) {
  152. expect.fail(`ProposalState ${state} out of possible states (0...${statesCount}-1)`);
  153. } else {
  154. result |= 1n << state;
  155. }
  156. }
  157. if (options.inverted) {
  158. const mask = 2n ** statesCount - 1n;
  159. result = result ^ mask;
  160. }
  161. return ethers.toBeHex(result, 32);
  162. }
  163. }
  164. module.exports = {
  165. GovernorHelper,
  166. timelockSalt,
  167. };