Votes.behavior.js 13 KB

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