governance.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  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(`unsuported 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. ? vote
  80. .signature(this.governor, {
  81. proposalId: proposal.id,
  82. support: vote.support,
  83. reason: vote.reason || '',
  84. params: vote.params || '',
  85. })
  86. .then(({ v, r, s }) =>
  87. this.governor.castVoteWithReasonAndParamsBySig(
  88. ...concatOpts([proposal.id, vote.support, vote.reason || '', vote.params || '', v, r, s], opts),
  89. ),
  90. )
  91. : vote
  92. .signature(this.governor, {
  93. proposalId: proposal.id,
  94. support: vote.support,
  95. })
  96. .then(({ v, r, s }) =>
  97. this.governor.castVoteBySig(...concatOpts([proposal.id, vote.support, v, r, s], opts)),
  98. )
  99. : vote.params
  100. ? // otherwise if params
  101. this.governor.castVoteWithReasonAndParams(
  102. ...concatOpts([proposal.id, vote.support, vote.reason || '', vote.params], opts),
  103. )
  104. : vote.reason
  105. ? // otherwise if reason
  106. this.governor.castVoteWithReason(...concatOpts([proposal.id, vote.support, vote.reason], opts))
  107. : this.governor.castVote(...concatOpts([proposal.id, vote.support], opts));
  108. }
  109. async waitForSnapshot(offset = 0) {
  110. const proposal = this.currentProposal;
  111. const timepoint = await this.governor.proposalSnapshot(proposal.id);
  112. return forward[this.mode](timepoint.addn(offset));
  113. }
  114. async waitForDeadline(offset = 0) {
  115. const proposal = this.currentProposal;
  116. const timepoint = await this.governor.proposalDeadline(proposal.id);
  117. return forward[this.mode](timepoint.addn(offset));
  118. }
  119. async waitForEta(offset = 0) {
  120. const proposal = this.currentProposal;
  121. const timestamp = await this.governor.proposalEta(proposal.id);
  122. return forward.timestamp(timestamp.addn(offset));
  123. }
  124. /**
  125. * Specify a proposal either as
  126. * 1) an array of objects [{ target, value, data, signature? }]
  127. * 2) an object of arrays { targets: [], values: [], data: [], signatures?: [] }
  128. */
  129. setProposal(actions, description) {
  130. let targets, values, signatures, data, useCompatibilityInterface;
  131. if (Array.isArray(actions)) {
  132. useCompatibilityInterface = actions.some(a => 'signature' in a);
  133. targets = actions.map(a => a.target);
  134. values = actions.map(a => a.value || '0');
  135. signatures = actions.map(a => a.signature || '');
  136. data = actions.map(a => a.data || '0x');
  137. } else {
  138. useCompatibilityInterface = Array.isArray(actions.signatures);
  139. ({ targets, values, signatures = [], data } = actions);
  140. }
  141. const fulldata = zip(
  142. signatures.map(s => s && web3.eth.abi.encodeFunctionSignature(s)),
  143. data,
  144. ).map(hexs => concatHex(...hexs));
  145. const descriptionHash = web3.utils.keccak256(description);
  146. // condensed version for queueing end executing
  147. const shortProposal = [targets, values, fulldata, descriptionHash];
  148. // full version for proposing
  149. const fullProposal = [targets, values, ...(useCompatibilityInterface ? [signatures] : []), data, description];
  150. // proposal id
  151. const id = web3.utils.toBN(
  152. web3.utils.keccak256(
  153. web3.eth.abi.encodeParameters(['address[]', 'uint256[]', 'bytes[]', 'bytes32'], shortProposal),
  154. ),
  155. );
  156. this.currentProposal = {
  157. id,
  158. targets,
  159. values,
  160. signatures,
  161. data,
  162. fulldata,
  163. description,
  164. descriptionHash,
  165. shortProposal,
  166. fullProposal,
  167. useCompatibilityInterface,
  168. };
  169. return this.currentProposal;
  170. }
  171. }
  172. /**
  173. * Encodes a list ProposalStates into a bytes32 representation where each bit enabled corresponds to
  174. * the underlying position in the `ProposalState` enum. For example:
  175. *
  176. * 0x000...10000
  177. * ^^^^^^------ ...
  178. * ^----- Succeeded
  179. * ^---- Defeated
  180. * ^--- Canceled
  181. * ^-- Active
  182. * ^- Pending
  183. */
  184. function proposalStatesToBitMap(proposalStates, options = {}) {
  185. if (!Array.isArray(proposalStates)) {
  186. proposalStates = [proposalStates];
  187. }
  188. const statesCount = Object.keys(ProposalState).length;
  189. let result = 0;
  190. const uniqueProposalStates = new Set(proposalStates.map(bn => bn.toNumber())); // Remove duplicates
  191. for (const state of uniqueProposalStates) {
  192. if (state < 0 || state >= statesCount) {
  193. expect.fail(`ProposalState ${state} out of possible states (0...${statesCount}-1)`);
  194. } else {
  195. result |= 1 << state;
  196. }
  197. }
  198. if (options.inverted) {
  199. const mask = 2 ** statesCount - 1;
  200. result = result ^ mask;
  201. }
  202. const hex = web3.utils.numberToHex(result);
  203. return web3.utils.padLeft(hex, 64);
  204. }
  205. module.exports = {
  206. GovernorHelper,
  207. proposalStatesToBitMap,
  208. };