Votes.behavior.js 14 KB

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