governance.js 6.1 KB

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