Votes.behavior.js 14 KB

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