Votes.behavior.js 14 KB

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