Votes.behavior.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  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 { EIP712Domain, domainSeparator } = require('../../helpers/eip712');
  7. const { web3 } = require('hardhat');
  8. const Delegation = [
  9. { name: 'delegatee', type: 'address' },
  10. { name: 'nonce', type: 'uint256' },
  11. { name: 'expiry', type: 'uint256' },
  12. ];
  13. const version = '1';
  14. function shouldBehaveLikeVotes (accounts, tokens, fungible = true) {
  15. const getWeight = token => web3.utils.toBN(fungible ? token : 1);
  16. describe('run votes workflow', function () {
  17. it('initial nonce is 0', async function () {
  18. expect(await this.votes.nonces(accounts[0])).to.be.bignumber.equal('0');
  19. });
  20. it('domain separator', async function () {
  21. expect(
  22. await this.votes.DOMAIN_SEPARATOR(),
  23. ).to.equal(
  24. await domainSeparator(this.name, version, this.chainId, this.votes.address),
  25. );
  26. });
  27. describe('delegation', function () {
  28. const token = tokens[0];
  29. it('delegation without tokens', async function () {
  30. expect(await this.votes.delegates(accounts[1])).to.be.equal(ZERO_ADDRESS);
  31. const { receipt } = await this.votes.delegate(accounts[1], { from: accounts[1] });
  32. expectEvent(receipt, 'DelegateChanged', {
  33. delegator: accounts[1],
  34. fromDelegate: ZERO_ADDRESS,
  35. toDelegate: accounts[1],
  36. });
  37. expectEvent.notEmitted(receipt, 'DelegateVotesChanged');
  38. expect(await this.votes.delegates(accounts[1])).to.be.equal(accounts[1]);
  39. });
  40. it('delegation with tokens', async function () {
  41. await this.votes.mint(accounts[1], token);
  42. const weight = getWeight(token);
  43. expect(await this.votes.delegates(accounts[1])).to.be.equal(ZERO_ADDRESS);
  44. const { receipt } = await this.votes.delegate(accounts[1], { from: accounts[1] });
  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. previousBalance: '0',
  53. newBalance: 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], receipt.blockNumber - 1)).to.be.bignumber.equal('0');
  58. await time.advanceBlock();
  59. expect(await this.votes.getPastVotes(accounts[1], receipt.blockNumber)).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. expectEvent(receipt, 'DelegateChanged', {
  70. delegator: accounts[1],
  71. fromDelegate: accounts[1],
  72. toDelegate: accounts[2],
  73. });
  74. expectEvent(receipt, 'DelegateVotesChanged', {
  75. delegate: accounts[1],
  76. previousBalance: weight,
  77. newBalance: '0',
  78. });
  79. expectEvent(receipt, 'DelegateVotesChanged', {
  80. delegate: accounts[2],
  81. previousBalance: '0',
  82. newBalance: weight,
  83. });
  84. expect(await this.votes.delegates(accounts[1])).to.be.equal(accounts[2]);
  85. expect(await this.votes.getVotes(accounts[1])).to.be.bignumber.equal('0');
  86. expect(await this.votes.getVotes(accounts[2])).to.be.bignumber.equal(weight);
  87. expect(await this.votes.getPastVotes(accounts[1], receipt.blockNumber - 1)).to.be.bignumber.equal(weight);
  88. expect(await this.votes.getPastVotes(accounts[2], receipt.blockNumber - 1)).to.be.bignumber.equal('0');
  89. await time.advanceBlock();
  90. expect(await this.votes.getPastVotes(accounts[1], receipt.blockNumber)).to.be.bignumber.equal('0');
  91. expect(await this.votes.getPastVotes(accounts[2], receipt.blockNumber)).to.be.bignumber.equal(weight);
  92. });
  93. describe('with signature', function () {
  94. const delegator = Wallet.generate();
  95. const [delegatee, other] = accounts;
  96. const nonce = 0;
  97. delegator.address = web3.utils.toChecksumAddress(delegator.getAddressString());
  98. const buildData = (chainId, verifyingContract, name, message) => ({
  99. data: {
  100. primaryType: 'Delegation',
  101. types: { EIP712Domain, Delegation },
  102. domain: { name, version, chainId, verifyingContract },
  103. message,
  104. },
  105. });
  106. it('accept signed delegation', async function () {
  107. await this.votes.mint(delegator.address, token);
  108. const weight = getWeight(token);
  109. const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage(
  110. delegator.getPrivateKey(),
  111. buildData(this.chainId, this.votes.address, this.name, {
  112. delegatee,
  113. nonce,
  114. expiry: MAX_UINT256,
  115. }),
  116. ));
  117. expect(await this.votes.delegates(delegator.address)).to.be.equal(ZERO_ADDRESS);
  118. const { receipt } = await this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s);
  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, receipt.blockNumber - 1)).to.be.bignumber.equal('0');
  133. await time.advanceBlock();
  134. expect(await this.votes.getPastVotes(delegatee, receipt.blockNumber)).to.be.bignumber.equal(weight);
  135. });
  136. it('rejects reused signature', async function () {
  137. const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage(
  138. delegator.getPrivateKey(),
  139. buildData(this.chainId, this.votes.address, this.name, {
  140. delegatee,
  141. nonce,
  142. expiry: MAX_UINT256,
  143. }),
  144. ));
  145. await this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s);
  146. await expectRevert(
  147. this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s),
  148. 'Votes: invalid nonce',
  149. );
  150. });
  151. it('rejects bad delegatee', async function () {
  152. const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage(
  153. delegator.getPrivateKey(),
  154. buildData(this.chainId, this.votes.address, this.name, {
  155. delegatee,
  156. nonce,
  157. expiry: MAX_UINT256,
  158. }),
  159. ));
  160. const receipt = await this.votes.delegateBySig(other, nonce, MAX_UINT256, v, r, s);
  161. const { args } = receipt.logs.find(({ event }) => event === 'DelegateChanged');
  162. expect(args.delegator).to.not.be.equal(delegator.address);
  163. expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS);
  164. expect(args.toDelegate).to.be.equal(other);
  165. });
  166. it('rejects bad nonce', async function () {
  167. const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage(
  168. delegator.getPrivateKey(),
  169. buildData(this.chainId, this.votes.address, this.name, {
  170. delegatee,
  171. nonce: nonce + 1,
  172. expiry: MAX_UINT256,
  173. }),
  174. ));
  175. await expectRevert(
  176. this.votes.delegateBySig(delegatee, nonce + 1, MAX_UINT256, v, r, s),
  177. 'Votes: invalid nonce',
  178. );
  179. });
  180. it('rejects expired permit', async function () {
  181. const expiry = (await time.latest()) - time.duration.weeks(1);
  182. const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage(
  183. delegator.getPrivateKey(),
  184. buildData(this.chainId, this.votes.address, this.name, {
  185. delegatee,
  186. nonce,
  187. expiry,
  188. }),
  189. ));
  190. await expectRevert(
  191. this.votes.delegateBySig(delegatee, nonce, expiry, v, r, s),
  192. 'Votes: signature expired',
  193. );
  194. });
  195. });
  196. });
  197. describe('getPastTotalSupply', function () {
  198. beforeEach(async function () {
  199. await this.votes.delegate(accounts[1], { from: accounts[1] });
  200. });
  201. it('reverts if block number >= current block', async function () {
  202. await expectRevert(
  203. this.votes.getPastTotalSupply(5e10),
  204. 'block not yet mined',
  205. );
  206. });
  207. it('returns 0 if there are no checkpoints', async function () {
  208. expect(await this.votes.getPastTotalSupply(0)).to.be.bignumber.equal('0');
  209. });
  210. it('returns the correct checkpointed total supply', async function () {
  211. const blockNumber = Number(await time.latestBlock());
  212. await this.votes.mint(accounts[1], tokens[0]); // mint 0
  213. await time.advanceBlock();
  214. await this.votes.mint(accounts[1], tokens[1]); // mint 1
  215. await time.advanceBlock();
  216. await this.votes.burn(...(fungible ? [accounts[1]] : []), tokens[1]); // burn 1
  217. await time.advanceBlock();
  218. await this.votes.mint(accounts[1], tokens[2]); // mint 2
  219. await time.advanceBlock();
  220. await this.votes.burn(...(fungible ? [accounts[1]] : []), tokens[0]); // burn 0
  221. await time.advanceBlock();
  222. await this.votes.burn(...(fungible ? [accounts[1]] : []), tokens[2]); // burn 2
  223. await time.advanceBlock();
  224. const weight = tokens.map(getWeight);
  225. expect(await this.votes.getPastTotalSupply(blockNumber)).to.be.bignumber.equal('0');
  226. expect(await this.votes.getPastTotalSupply(blockNumber + 1)).to.be.bignumber.equal(weight[0]);
  227. expect(await this.votes.getPastTotalSupply(blockNumber + 2)).to.be.bignumber.equal(weight[0]);
  228. expect(await this.votes.getPastTotalSupply(blockNumber + 3)).to.be.bignumber.equal(weight[0].add(weight[1]));
  229. expect(await this.votes.getPastTotalSupply(blockNumber + 4)).to.be.bignumber.equal(weight[0].add(weight[1]));
  230. expect(await this.votes.getPastTotalSupply(blockNumber + 5)).to.be.bignumber.equal(weight[0]);
  231. expect(await this.votes.getPastTotalSupply(blockNumber + 6)).to.be.bignumber.equal(weight[0]);
  232. expect(await this.votes.getPastTotalSupply(blockNumber + 7)).to.be.bignumber.equal(weight[0].add(weight[2]));
  233. expect(await this.votes.getPastTotalSupply(blockNumber + 8)).to.be.bignumber.equal(weight[0].add(weight[2]));
  234. expect(await this.votes.getPastTotalSupply(blockNumber + 9)).to.be.bignumber.equal(weight[2]);
  235. expect(await this.votes.getPastTotalSupply(blockNumber + 10)).to.be.bignumber.equal(weight[2]);
  236. expect(await this.votes.getPastTotalSupply(blockNumber + 11)).to.be.bignumber.equal('0');
  237. await expectRevert(this.votes.getPastTotalSupply(blockNumber + 12), 'Votes: block not yet mined');
  238. });
  239. });
  240. // The following tests are an adaptation of
  241. // https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js.
  242. describe('Compound test suite', function () {
  243. beforeEach(async function () {
  244. await this.votes.mint(accounts[1], tokens[0]);
  245. await this.votes.mint(accounts[1], tokens[1]);
  246. await this.votes.mint(accounts[1], tokens[2]);
  247. });
  248. describe('getPastVotes', function () {
  249. it('reverts if block number >= current block', async function () {
  250. await expectRevert(
  251. this.votes.getPastVotes(accounts[2], 5e10),
  252. 'block not yet mined',
  253. );
  254. });
  255. it('returns 0 if there are no checkpoints', async function () {
  256. expect(await this.votes.getPastVotes(accounts[2], 0)).to.be.bignumber.equal('0');
  257. });
  258. it('returns the latest block if >= last checkpoint block', async function () {
  259. const tx = await this.votes.delegate(accounts[2], { from: accounts[1] });
  260. await time.advanceBlock();
  261. await time.advanceBlock();
  262. const latest = await this.votes.getVotes(accounts[2]);
  263. expect(await this.votes.getPastVotes(accounts[2], tx.receipt.blockNumber)).to.be.bignumber.equal(latest);
  264. expect(await this.votes.getPastVotes(accounts[2], tx.receipt.blockNumber + 1)).to.be.bignumber.equal(latest);
  265. });
  266. it('returns zero if < first checkpoint block', async function () {
  267. await time.advanceBlock();
  268. const tx = await this.votes.delegate(accounts[2], { from: accounts[1] });
  269. await time.advanceBlock();
  270. await time.advanceBlock();
  271. expect(await this.votes.getPastVotes(accounts[2], tx.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
  272. });
  273. });
  274. });
  275. });
  276. }
  277. module.exports = {
  278. shouldBehaveLikeVotes,
  279. };