Votes.behavior.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. const { constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
  2. const { MAX_UINT256, ZERO_ADDRESS } = constants;
  3. const { fromRpcSig } = require('ethereumjs-util');
  4. const ethSigUtil = require('eth-sig-util');
  5. const Wallet = require('ethereumjs-wallet').default;
  6. const { shouldBehaveLikeEIP6372 } = require('./EIP6372.behavior');
  7. const { getDomain, domainType } = require('../../helpers/eip712');
  8. const { clockFromReceipt } = require('../../helpers/time');
  9. const Delegation = [
  10. { name: 'delegatee', type: 'address' },
  11. { name: 'nonce', type: 'uint256' },
  12. { name: 'expiry', type: 'uint256' },
  13. ];
  14. const buildAndSignDelegation = (contract, message, pk) =>
  15. getDomain(contract)
  16. .then(domain => ({
  17. primaryType: 'Delegation',
  18. types: { EIP712Domain: domainType(domain), Delegation },
  19. domain,
  20. message,
  21. }))
  22. .then(data => fromRpcSig(ethSigUtil.signTypedMessage(pk, { data })));
  23. function shouldBehaveLikeVotes(accounts, tokens, { mode = 'blocknumber', fungible = true }) {
  24. shouldBehaveLikeEIP6372(mode);
  25. const getWeight = token => web3.utils.toBN(fungible ? token : 1);
  26. describe('run votes workflow', function () {
  27. it('initial nonce is 0', async function () {
  28. expect(await this.votes.nonces(accounts[0])).to.be.bignumber.equal('0');
  29. });
  30. describe('delegation with signature', function () {
  31. const token = tokens[0];
  32. it('delegation without tokens', async function () {
  33. expect(await this.votes.delegates(accounts[1])).to.be.equal(ZERO_ADDRESS);
  34. const { receipt } = await this.votes.delegate(accounts[1], { from: accounts[1] });
  35. expectEvent(receipt, 'DelegateChanged', {
  36. delegator: accounts[1],
  37. fromDelegate: ZERO_ADDRESS,
  38. toDelegate: accounts[1],
  39. });
  40. expectEvent.notEmitted(receipt, 'DelegateVotesChanged');
  41. expect(await this.votes.delegates(accounts[1])).to.be.equal(accounts[1]);
  42. });
  43. it('delegation with tokens', async function () {
  44. await this.votes.$_mint(accounts[1], token);
  45. const weight = getWeight(token);
  46. expect(await this.votes.delegates(accounts[1])).to.be.equal(ZERO_ADDRESS);
  47. const { receipt } = await this.votes.delegate(accounts[1], { from: accounts[1] });
  48. const timepoint = await clockFromReceipt[mode](receipt);
  49. expectEvent(receipt, 'DelegateChanged', {
  50. delegator: accounts[1],
  51. fromDelegate: ZERO_ADDRESS,
  52. toDelegate: accounts[1],
  53. });
  54. expectEvent(receipt, 'DelegateVotesChanged', {
  55. delegate: accounts[1],
  56. previousBalance: '0',
  57. newBalance: weight,
  58. });
  59. expect(await this.votes.delegates(accounts[1])).to.be.equal(accounts[1]);
  60. expect(await this.votes.getVotes(accounts[1])).to.be.bignumber.equal(weight);
  61. expect(await this.votes.getPastVotes(accounts[1], timepoint - 1)).to.be.bignumber.equal('0');
  62. await time.advanceBlock();
  63. expect(await this.votes.getPastVotes(accounts[1], timepoint)).to.be.bignumber.equal(weight);
  64. });
  65. it('delegation update', async function () {
  66. await this.votes.delegate(accounts[1], { from: accounts[1] });
  67. await this.votes.$_mint(accounts[1], token);
  68. const weight = getWeight(token);
  69. expect(await this.votes.delegates(accounts[1])).to.be.equal(accounts[1]);
  70. expect(await this.votes.getVotes(accounts[1])).to.be.bignumber.equal(weight);
  71. expect(await this.votes.getVotes(accounts[2])).to.be.bignumber.equal('0');
  72. const { receipt } = await this.votes.delegate(accounts[2], { from: accounts[1] });
  73. const timepoint = await clockFromReceipt[mode](receipt);
  74. expectEvent(receipt, 'DelegateChanged', {
  75. delegator: accounts[1],
  76. fromDelegate: accounts[1],
  77. toDelegate: accounts[2],
  78. });
  79. expectEvent(receipt, 'DelegateVotesChanged', {
  80. delegate: accounts[1],
  81. previousBalance: weight,
  82. newBalance: '0',
  83. });
  84. expectEvent(receipt, 'DelegateVotesChanged', {
  85. delegate: accounts[2],
  86. previousBalance: '0',
  87. newBalance: weight,
  88. });
  89. expect(await this.votes.delegates(accounts[1])).to.be.equal(accounts[2]);
  90. expect(await this.votes.getVotes(accounts[1])).to.be.bignumber.equal('0');
  91. expect(await this.votes.getVotes(accounts[2])).to.be.bignumber.equal(weight);
  92. expect(await this.votes.getPastVotes(accounts[1], timepoint - 1)).to.be.bignumber.equal(weight);
  93. expect(await this.votes.getPastVotes(accounts[2], timepoint - 1)).to.be.bignumber.equal('0');
  94. await time.advanceBlock();
  95. expect(await this.votes.getPastVotes(accounts[1], timepoint)).to.be.bignumber.equal('0');
  96. expect(await this.votes.getPastVotes(accounts[2], timepoint)).to.be.bignumber.equal(weight);
  97. });
  98. describe('with signature', function () {
  99. const delegator = Wallet.generate();
  100. const [delegatee, other] = accounts;
  101. const nonce = 0;
  102. delegator.address = web3.utils.toChecksumAddress(delegator.getAddressString());
  103. it('accept signed delegation', async function () {
  104. await this.votes.$_mint(delegator.address, token);
  105. const weight = getWeight(token);
  106. const { v, r, s } = await buildAndSignDelegation(
  107. this.votes,
  108. {
  109. delegatee,
  110. nonce,
  111. expiry: MAX_UINT256,
  112. },
  113. delegator.getPrivateKey(),
  114. );
  115. expect(await this.votes.delegates(delegator.address)).to.be.equal(ZERO_ADDRESS);
  116. const { receipt } = await this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s);
  117. const timepoint = await clockFromReceipt[mode](receipt);
  118. expectEvent(receipt, 'DelegateChanged', {
  119. delegator: delegator.address,
  120. fromDelegate: ZERO_ADDRESS,
  121. toDelegate: delegatee,
  122. });
  123. expectEvent(receipt, 'DelegateVotesChanged', {
  124. delegate: delegatee,
  125. previousBalance: '0',
  126. newBalance: weight,
  127. });
  128. expect(await this.votes.delegates(delegator.address)).to.be.equal(delegatee);
  129. expect(await this.votes.getVotes(delegator.address)).to.be.bignumber.equal('0');
  130. expect(await this.votes.getVotes(delegatee)).to.be.bignumber.equal(weight);
  131. expect(await this.votes.getPastVotes(delegatee, timepoint - 1)).to.be.bignumber.equal('0');
  132. await time.advanceBlock();
  133. expect(await this.votes.getPastVotes(delegatee, timepoint)).to.be.bignumber.equal(weight);
  134. });
  135. it('rejects reused signature', async function () {
  136. const { v, r, s } = await buildAndSignDelegation(
  137. this.votes,
  138. {
  139. delegatee,
  140. nonce,
  141. expiry: MAX_UINT256,
  142. },
  143. delegator.getPrivateKey(),
  144. );
  145. await this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s);
  146. await expectRevert(this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s), 'Votes: invalid nonce');
  147. });
  148. it('rejects bad delegatee', async function () {
  149. const { v, r, s } = await buildAndSignDelegation(
  150. this.votes,
  151. {
  152. delegatee,
  153. nonce,
  154. expiry: MAX_UINT256,
  155. },
  156. delegator.getPrivateKey(),
  157. );
  158. const receipt = await this.votes.delegateBySig(other, nonce, MAX_UINT256, v, r, s);
  159. const { args } = receipt.logs.find(({ event }) => event === 'DelegateChanged');
  160. expect(args.delegator).to.not.be.equal(delegator.address);
  161. expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS);
  162. expect(args.toDelegate).to.be.equal(other);
  163. });
  164. it('rejects bad nonce', async function () {
  165. const { v, r, s } = await buildAndSignDelegation(
  166. this.votes,
  167. {
  168. delegatee,
  169. nonce: nonce + 1,
  170. expiry: MAX_UINT256,
  171. },
  172. delegator.getPrivateKey(),
  173. );
  174. await expectRevert(
  175. this.votes.delegateBySig(delegatee, nonce + 1, MAX_UINT256, v, r, s),
  176. 'Votes: invalid nonce',
  177. );
  178. });
  179. it('rejects expired permit', async function () {
  180. const expiry = (await time.latest()) - time.duration.weeks(1);
  181. const { v, r, s } = await buildAndSignDelegation(
  182. this.votes,
  183. {
  184. delegatee,
  185. nonce,
  186. expiry,
  187. },
  188. delegator.getPrivateKey(),
  189. );
  190. await expectRevert(this.votes.delegateBySig(delegatee, nonce, expiry, v, r, s), 'Votes: signature expired');
  191. });
  192. });
  193. });
  194. describe('getPastTotalSupply', function () {
  195. beforeEach(async function () {
  196. await this.votes.delegate(accounts[1], { from: accounts[1] });
  197. });
  198. it('reverts if block number >= current block', async function () {
  199. await expectRevert(this.votes.getPastTotalSupply(5e10), 'future lookup');
  200. });
  201. it('returns 0 if there are no checkpoints', async function () {
  202. expect(await this.votes.getPastTotalSupply(0)).to.be.bignumber.equal('0');
  203. });
  204. it('returns the correct checkpointed total supply', async function () {
  205. const weight = tokens.map(token => getWeight(token));
  206. // t0 = mint #0
  207. const t0 = await this.votes.$_mint(accounts[1], tokens[0]);
  208. await time.advanceBlock();
  209. // t1 = mint #1
  210. const t1 = await this.votes.$_mint(accounts[1], tokens[1]);
  211. await time.advanceBlock();
  212. // t2 = burn #1
  213. const t2 = await this.votes.$_burn(...(fungible ? [accounts[1]] : []), tokens[1]);
  214. await time.advanceBlock();
  215. // t3 = mint #2
  216. const t3 = await this.votes.$_mint(accounts[1], tokens[2]);
  217. await time.advanceBlock();
  218. // t4 = burn #0
  219. const t4 = await this.votes.$_burn(...(fungible ? [accounts[1]] : []), tokens[0]);
  220. await time.advanceBlock();
  221. // t5 = burn #2
  222. const t5 = await this.votes.$_burn(...(fungible ? [accounts[1]] : []), tokens[2]);
  223. await time.advanceBlock();
  224. t0.timepoint = await clockFromReceipt[mode](t0.receipt);
  225. t1.timepoint = await clockFromReceipt[mode](t1.receipt);
  226. t2.timepoint = await clockFromReceipt[mode](t2.receipt);
  227. t3.timepoint = await clockFromReceipt[mode](t3.receipt);
  228. t4.timepoint = await clockFromReceipt[mode](t4.receipt);
  229. t5.timepoint = await clockFromReceipt[mode](t5.receipt);
  230. expect(await this.votes.getPastTotalSupply(t0.timepoint - 1)).to.be.bignumber.equal('0');
  231. expect(await this.votes.getPastTotalSupply(t0.timepoint)).to.be.bignumber.equal(weight[0]);
  232. expect(await this.votes.getPastTotalSupply(t0.timepoint + 1)).to.be.bignumber.equal(weight[0]);
  233. expect(await this.votes.getPastTotalSupply(t1.timepoint)).to.be.bignumber.equal(weight[0].add(weight[1]));
  234. expect(await this.votes.getPastTotalSupply(t1.timepoint + 1)).to.be.bignumber.equal(weight[0].add(weight[1]));
  235. expect(await this.votes.getPastTotalSupply(t2.timepoint)).to.be.bignumber.equal(weight[0]);
  236. expect(await this.votes.getPastTotalSupply(t2.timepoint + 1)).to.be.bignumber.equal(weight[0]);
  237. expect(await this.votes.getPastTotalSupply(t3.timepoint)).to.be.bignumber.equal(weight[0].add(weight[2]));
  238. expect(await this.votes.getPastTotalSupply(t3.timepoint + 1)).to.be.bignumber.equal(weight[0].add(weight[2]));
  239. expect(await this.votes.getPastTotalSupply(t4.timepoint)).to.be.bignumber.equal(weight[2]);
  240. expect(await this.votes.getPastTotalSupply(t4.timepoint + 1)).to.be.bignumber.equal(weight[2]);
  241. expect(await this.votes.getPastTotalSupply(t5.timepoint)).to.be.bignumber.equal('0');
  242. await expectRevert(this.votes.getPastTotalSupply(t5.timepoint + 1), 'Votes: future lookup');
  243. });
  244. });
  245. // The following tests are an adaptation of
  246. // https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js.
  247. describe('Compound test suite', function () {
  248. beforeEach(async function () {
  249. await this.votes.$_mint(accounts[1], tokens[0]);
  250. await this.votes.$_mint(accounts[1], tokens[1]);
  251. await this.votes.$_mint(accounts[1], tokens[2]);
  252. });
  253. describe('getPastVotes', function () {
  254. it('reverts if block number >= current block', async function () {
  255. await expectRevert(this.votes.getPastVotes(accounts[2], 5e10), 'future lookup');
  256. });
  257. it('returns 0 if there are no checkpoints', async function () {
  258. expect(await this.votes.getPastVotes(accounts[2], 0)).to.be.bignumber.equal('0');
  259. });
  260. it('returns the latest block if >= last checkpoint block', async function () {
  261. const { receipt } = await this.votes.delegate(accounts[2], { from: accounts[1] });
  262. const timepoint = await clockFromReceipt[mode](receipt);
  263. await time.advanceBlock();
  264. await time.advanceBlock();
  265. const latest = await this.votes.getVotes(accounts[2]);
  266. expect(await this.votes.getPastVotes(accounts[2], timepoint)).to.be.bignumber.equal(latest);
  267. expect(await this.votes.getPastVotes(accounts[2], timepoint + 1)).to.be.bignumber.equal(latest);
  268. });
  269. it('returns zero if < first checkpoint block', async function () {
  270. await time.advanceBlock();
  271. const { receipt } = await this.votes.delegate(accounts[2], { from: accounts[1] });
  272. const timepoint = await clockFromReceipt[mode](receipt);
  273. await time.advanceBlock();
  274. await time.advanceBlock();
  275. expect(await this.votes.getPastVotes(accounts[2], timepoint - 1)).to.be.bignumber.equal('0');
  276. });
  277. });
  278. });
  279. });
  280. }
  281. module.exports = {
  282. shouldBehaveLikeVotes,
  283. };