Votes.behavior.js 15 KB

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