governance.js 7.7 KB

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