governance.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. const { web3 } = require('hardhat');
  2. const { forward } = require('../helpers/time');
  3. const { ProposalState } = require('./enums');
  4. function zip(...args) {
  5. return Array(Math.max(...args.map(array => array.length)))
  6. .fill()
  7. .map((_, i) => args.map(array => array[i]));
  8. }
  9. function concatHex(...args) {
  10. return web3.utils.bytesToHex([].concat(...args.map(h => web3.utils.hexToBytes(h || '0x'))));
  11. }
  12. function concatOpts(args, opts = null) {
  13. return opts ? args.concat(opts) : args;
  14. }
  15. const timelockSalt = (address, descriptionHash) =>
  16. '0x' + web3.utils.toBN(address).shln(96).xor(web3.utils.toBN(descriptionHash)).toString(16, 64);
  17. class GovernorHelper {
  18. constructor(governor, mode = 'blocknumber') {
  19. this.governor = governor;
  20. this.mode = mode;
  21. }
  22. delegate(delegation = {}, opts = null) {
  23. return Promise.all([
  24. delegation.token.delegate(delegation.to, { from: delegation.to }),
  25. delegation.value && delegation.token.transfer(...concatOpts([delegation.to, delegation.value]), opts),
  26. delegation.tokenId &&
  27. delegation.token
  28. .ownerOf(delegation.tokenId)
  29. .then(owner =>
  30. delegation.token.transferFrom(...concatOpts([owner, delegation.to, delegation.tokenId], opts)),
  31. ),
  32. ]);
  33. }
  34. propose(opts = null) {
  35. const proposal = this.currentProposal;
  36. return this.governor.methods[
  37. proposal.useCompatibilityInterface
  38. ? 'propose(address[],uint256[],string[],bytes[],string)'
  39. : 'propose(address[],uint256[],bytes[],string)'
  40. ](...concatOpts(proposal.fullProposal, opts));
  41. }
  42. queue(opts = null) {
  43. const proposal = this.currentProposal;
  44. return proposal.useCompatibilityInterface
  45. ? this.governor.methods['queue(uint256)'](...concatOpts([proposal.id], opts))
  46. : this.governor.methods['queue(address[],uint256[],bytes[],bytes32)'](
  47. ...concatOpts(proposal.shortProposal, opts),
  48. );
  49. }
  50. execute(opts = null) {
  51. const proposal = this.currentProposal;
  52. return proposal.useCompatibilityInterface
  53. ? this.governor.methods['execute(uint256)'](...concatOpts([proposal.id], opts))
  54. : this.governor.methods['execute(address[],uint256[],bytes[],bytes32)'](
  55. ...concatOpts(proposal.shortProposal, opts),
  56. );
  57. }
  58. cancel(visibility = 'external', opts = null) {
  59. const proposal = this.currentProposal;
  60. switch (visibility) {
  61. case 'external':
  62. if (proposal.useCompatibilityInterface) {
  63. return this.governor.methods['cancel(uint256)'](...concatOpts([proposal.id], opts));
  64. } else {
  65. return this.governor.methods['cancel(address[],uint256[],bytes[],bytes32)'](
  66. ...concatOpts(proposal.shortProposal, opts),
  67. );
  68. }
  69. case 'internal':
  70. return this.governor.methods['$_cancel(address[],uint256[],bytes[],bytes32)'](
  71. ...concatOpts(proposal.shortProposal, opts),
  72. );
  73. default:
  74. throw new Error(`unsupported visibility "${visibility}"`);
  75. }
  76. }
  77. vote(vote = {}, opts = null) {
  78. const proposal = this.currentProposal;
  79. return vote.signature
  80. ? // if signature, and either params or reason →
  81. vote.params || vote.reason
  82. ? this.sign(vote).then(signature =>
  83. this.governor.castVoteWithReasonAndParamsBySig(
  84. ...concatOpts(
  85. [proposal.id, vote.support, vote.voter, vote.reason || '', vote.params || '', signature],
  86. opts,
  87. ),
  88. ),
  89. )
  90. : this.sign(vote).then(signature =>
  91. this.governor.castVoteBySig(...concatOpts([proposal.id, vote.support, vote.voter, signature], opts)),
  92. )
  93. : vote.params
  94. ? // otherwise if params
  95. this.governor.castVoteWithReasonAndParams(
  96. ...concatOpts([proposal.id, vote.support, vote.reason || '', vote.params], opts),
  97. )
  98. : vote.reason
  99. ? // otherwise if reason
  100. this.governor.castVoteWithReason(...concatOpts([proposal.id, vote.support, vote.reason], opts))
  101. : this.governor.castVote(...concatOpts([proposal.id, vote.support], opts));
  102. }
  103. sign(vote = {}) {
  104. return vote.signature(this.governor, this.forgeMessage(vote));
  105. }
  106. forgeMessage(vote = {}) {
  107. const proposal = this.currentProposal;
  108. const message = { proposalId: proposal.id, support: vote.support, voter: vote.voter, nonce: vote.nonce };
  109. if (vote.params || vote.reason) {
  110. message.reason = vote.reason || '';
  111. message.params = vote.params || '';
  112. }
  113. return message;
  114. }
  115. async waitForSnapshot(offset = 0) {
  116. const proposal = this.currentProposal;
  117. const timepoint = await this.governor.proposalSnapshot(proposal.id);
  118. return forward[this.mode](timepoint.addn(offset));
  119. }
  120. async waitForDeadline(offset = 0) {
  121. const proposal = this.currentProposal;
  122. const timepoint = await this.governor.proposalDeadline(proposal.id);
  123. return forward[this.mode](timepoint.addn(offset));
  124. }
  125. async waitForEta(offset = 0) {
  126. const proposal = this.currentProposal;
  127. const timestamp = await this.governor.proposalEta(proposal.id);
  128. return forward.timestamp(timestamp.addn(offset));
  129. }
  130. /**
  131. * Specify a proposal either as
  132. * 1) an array of objects [{ target, value, data, signature? }]
  133. * 2) an object of arrays { targets: [], values: [], data: [], signatures?: [] }
  134. */
  135. setProposal(actions, description) {
  136. let targets, values, signatures, data, useCompatibilityInterface;
  137. if (Array.isArray(actions)) {
  138. useCompatibilityInterface = actions.some(a => 'signature' in a);
  139. targets = actions.map(a => a.target);
  140. values = actions.map(a => a.value || '0');
  141. signatures = actions.map(a => a.signature || '');
  142. data = actions.map(a => a.data || '0x');
  143. } else {
  144. useCompatibilityInterface = Array.isArray(actions.signatures);
  145. ({ targets, values, signatures = [], data } = actions);
  146. }
  147. const fulldata = zip(
  148. signatures.map(s => s && web3.eth.abi.encodeFunctionSignature(s)),
  149. data,
  150. ).map(hexs => concatHex(...hexs));
  151. const descriptionHash = web3.utils.keccak256(description);
  152. // condensed version for queueing end executing
  153. const shortProposal = [targets, values, fulldata, descriptionHash];
  154. // full version for proposing
  155. const fullProposal = [targets, values, ...(useCompatibilityInterface ? [signatures] : []), data, description];
  156. // proposal id
  157. const id = web3.utils.toBN(
  158. web3.utils.keccak256(
  159. web3.eth.abi.encodeParameters(['address[]', 'uint256[]', 'bytes[]', 'bytes32'], shortProposal),
  160. ),
  161. );
  162. this.currentProposal = {
  163. id,
  164. targets,
  165. values,
  166. signatures,
  167. data,
  168. fulldata,
  169. description,
  170. descriptionHash,
  171. shortProposal,
  172. fullProposal,
  173. useCompatibilityInterface,
  174. };
  175. return this.currentProposal;
  176. }
  177. }
  178. /**
  179. * Encodes a list ProposalStates into a bytes32 representation where each bit enabled corresponds to
  180. * the underlying position in the `ProposalState` enum. For example:
  181. *
  182. * 0x000...10000
  183. * ^^^^^^------ ...
  184. * ^----- Succeeded
  185. * ^---- Defeated
  186. * ^--- Canceled
  187. * ^-- Active
  188. * ^- Pending
  189. */
  190. function proposalStatesToBitMap(proposalStates, options = {}) {
  191. if (!Array.isArray(proposalStates)) {
  192. proposalStates = [proposalStates];
  193. }
  194. const statesCount = Object.keys(ProposalState).length;
  195. let result = 0;
  196. const uniqueProposalStates = new Set(proposalStates.map(bn => bn.toNumber())); // Remove duplicates
  197. for (const state of uniqueProposalStates) {
  198. if (state < 0 || state >= statesCount) {
  199. expect.fail(`ProposalState ${state} out of possible states (0...${statesCount}-1)`);
  200. } else {
  201. result |= 1 << state;
  202. }
  203. }
  204. if (options.inverted) {
  205. const mask = 2 ** statesCount - 1;
  206. result = result ^ mask;
  207. }
  208. const hex = web3.utils.numberToHex(result);
  209. return web3.utils.padLeft(hex, 64);
  210. }
  211. module.exports = {
  212. GovernorHelper,
  213. proposalStatesToBitMap,
  214. timelockSalt,
  215. };