Votes.behavior.js 14 KB

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