Votes.behavior.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  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(await this.votes.DOMAIN_SEPARATOR()).to.equal(
  22. await domainSeparator({
  23. name: this.name,
  24. version,
  25. chainId: this.chainId,
  26. verifyingContract: this.votes.address,
  27. }),
  28. );
  29. });
  30. describe('delegation', 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. expectEvent(receipt, 'DelegateChanged', {
  49. delegator: accounts[1],
  50. fromDelegate: ZERO_ADDRESS,
  51. toDelegate: accounts[1],
  52. });
  53. expectEvent(receipt, 'DelegateVotesChanged', {
  54. delegate: accounts[1],
  55. previousBalance: '0',
  56. newBalance: weight,
  57. });
  58. expect(await this.votes.delegates(accounts[1])).to.be.equal(accounts[1]);
  59. expect(await this.votes.getVotes(accounts[1])).to.be.bignumber.equal(weight);
  60. expect(await this.votes.getPastVotes(accounts[1], receipt.blockNumber - 1)).to.be.bignumber.equal('0');
  61. await time.advanceBlock();
  62. expect(await this.votes.getPastVotes(accounts[1], receipt.blockNumber)).to.be.bignumber.equal(weight);
  63. });
  64. it('delegation update', async function () {
  65. await this.votes.delegate(accounts[1], { from: accounts[1] });
  66. await this.votes.$_mint(accounts[1], token);
  67. const weight = getWeight(token);
  68. expect(await this.votes.delegates(accounts[1])).to.be.equal(accounts[1]);
  69. expect(await this.votes.getVotes(accounts[1])).to.be.bignumber.equal(weight);
  70. expect(await this.votes.getVotes(accounts[2])).to.be.bignumber.equal('0');
  71. const { receipt } = await this.votes.delegate(accounts[2], { from: accounts[1] });
  72. expectEvent(receipt, 'DelegateChanged', {
  73. delegator: accounts[1],
  74. fromDelegate: accounts[1],
  75. toDelegate: accounts[2],
  76. });
  77. expectEvent(receipt, 'DelegateVotesChanged', {
  78. delegate: accounts[1],
  79. previousBalance: weight,
  80. newBalance: '0',
  81. });
  82. expectEvent(receipt, 'DelegateVotesChanged', {
  83. delegate: accounts[2],
  84. previousBalance: '0',
  85. newBalance: weight,
  86. });
  87. expect(await this.votes.delegates(accounts[1])).to.be.equal(accounts[2]);
  88. expect(await this.votes.getVotes(accounts[1])).to.be.bignumber.equal('0');
  89. expect(await this.votes.getVotes(accounts[2])).to.be.bignumber.equal(weight);
  90. expect(await this.votes.getPastVotes(accounts[1], receipt.blockNumber - 1)).to.be.bignumber.equal(weight);
  91. expect(await this.votes.getPastVotes(accounts[2], receipt.blockNumber - 1)).to.be.bignumber.equal('0');
  92. await time.advanceBlock();
  93. expect(await this.votes.getPastVotes(accounts[1], receipt.blockNumber)).to.be.bignumber.equal('0');
  94. expect(await this.votes.getPastVotes(accounts[2], receipt.blockNumber)).to.be.bignumber.equal(weight);
  95. });
  96. describe('with signature', function () {
  97. const delegator = Wallet.generate();
  98. const [delegatee, other] = accounts;
  99. const nonce = 0;
  100. delegator.address = web3.utils.toChecksumAddress(delegator.getAddressString());
  101. const buildData = (chainId, verifyingContract, name, message) => ({
  102. data: {
  103. primaryType: 'Delegation',
  104. types: { EIP712Domain, Delegation },
  105. domain: { name, version, chainId, verifyingContract },
  106. message,
  107. },
  108. });
  109. it('accept signed delegation', async function () {
  110. await this.votes.$_mint(delegator.address, token);
  111. const weight = getWeight(token);
  112. const { v, r, s } = fromRpcSig(
  113. ethSigUtil.signTypedMessage(
  114. delegator.getPrivateKey(),
  115. buildData(this.chainId, this.votes.address, this.name, {
  116. delegatee,
  117. nonce,
  118. expiry: MAX_UINT256,
  119. }),
  120. ),
  121. );
  122. expect(await this.votes.delegates(delegator.address)).to.be.equal(ZERO_ADDRESS);
  123. const { receipt } = await this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s);
  124. expectEvent(receipt, 'DelegateChanged', {
  125. delegator: delegator.address,
  126. fromDelegate: ZERO_ADDRESS,
  127. toDelegate: delegatee,
  128. });
  129. expectEvent(receipt, 'DelegateVotesChanged', {
  130. delegate: delegatee,
  131. previousBalance: '0',
  132. newBalance: weight,
  133. });
  134. expect(await this.votes.delegates(delegator.address)).to.be.equal(delegatee);
  135. expect(await this.votes.getVotes(delegator.address)).to.be.bignumber.equal('0');
  136. expect(await this.votes.getVotes(delegatee)).to.be.bignumber.equal(weight);
  137. expect(await this.votes.getPastVotes(delegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0');
  138. await time.advanceBlock();
  139. expect(await this.votes.getPastVotes(delegatee, receipt.blockNumber)).to.be.bignumber.equal(weight);
  140. });
  141. it('rejects reused signature', async function () {
  142. const { v, r, s } = fromRpcSig(
  143. ethSigUtil.signTypedMessage(
  144. delegator.getPrivateKey(),
  145. buildData(this.chainId, this.votes.address, this.name, {
  146. delegatee,
  147. nonce,
  148. expiry: MAX_UINT256,
  149. }),
  150. ),
  151. );
  152. await this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s);
  153. await expectRevert(this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s), 'Votes: invalid nonce');
  154. });
  155. it('rejects bad delegatee', async function () {
  156. const { v, r, s } = fromRpcSig(
  157. ethSigUtil.signTypedMessage(
  158. delegator.getPrivateKey(),
  159. buildData(this.chainId, this.votes.address, this.name, {
  160. delegatee,
  161. nonce,
  162. expiry: MAX_UINT256,
  163. }),
  164. ),
  165. );
  166. const receipt = await this.votes.delegateBySig(other, nonce, MAX_UINT256, v, r, s);
  167. const { args } = receipt.logs.find(({ event }) => event === 'DelegateChanged');
  168. expect(args.delegator).to.not.be.equal(delegator.address);
  169. expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS);
  170. expect(args.toDelegate).to.be.equal(other);
  171. });
  172. it('rejects bad nonce', async function () {
  173. const { v, r, s } = fromRpcSig(
  174. ethSigUtil.signTypedMessage(
  175. delegator.getPrivateKey(),
  176. buildData(this.chainId, this.votes.address, this.name, {
  177. delegatee,
  178. nonce: nonce + 1,
  179. expiry: MAX_UINT256,
  180. }),
  181. ),
  182. );
  183. await expectRevert(
  184. this.votes.delegateBySig(delegatee, nonce + 1, MAX_UINT256, v, r, s),
  185. 'Votes: invalid nonce',
  186. );
  187. });
  188. it('rejects expired permit', async function () {
  189. const expiry = (await time.latest()) - time.duration.weeks(1);
  190. const { v, r, s } = fromRpcSig(
  191. ethSigUtil.signTypedMessage(
  192. delegator.getPrivateKey(),
  193. buildData(this.chainId, this.votes.address, this.name, {
  194. delegatee,
  195. nonce,
  196. expiry,
  197. }),
  198. ),
  199. );
  200. await expectRevert(this.votes.delegateBySig(delegatee, nonce, expiry, v, r, s), 'Votes: signature expired');
  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. await expectRevert(this.votes.getPastTotalSupply(5e10), 'block not yet mined');
  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 blockNumber = Number(await time.latestBlock());
  216. await this.votes.$_mint(accounts[1], tokens[0]); // mint 0
  217. await time.advanceBlock();
  218. await this.votes.$_mint(accounts[1], tokens[1]); // mint 1
  219. await time.advanceBlock();
  220. await this.votes.$_burn(...(fungible ? [accounts[1]] : []), tokens[1]); // burn 1
  221. await time.advanceBlock();
  222. await this.votes.$_mint(accounts[1], tokens[2]); // mint 2
  223. await time.advanceBlock();
  224. await this.votes.$_burn(...(fungible ? [accounts[1]] : []), tokens[0]); // burn 0
  225. await time.advanceBlock();
  226. await this.votes.$_burn(...(fungible ? [accounts[1]] : []), tokens[2]); // burn 2
  227. await time.advanceBlock();
  228. const weight = tokens.map(getWeight);
  229. expect(await this.votes.getPastTotalSupply(blockNumber)).to.be.bignumber.equal('0');
  230. expect(await this.votes.getPastTotalSupply(blockNumber + 1)).to.be.bignumber.equal(weight[0]);
  231. expect(await this.votes.getPastTotalSupply(blockNumber + 2)).to.be.bignumber.equal(weight[0]);
  232. expect(await this.votes.getPastTotalSupply(blockNumber + 3)).to.be.bignumber.equal(weight[0].add(weight[1]));
  233. expect(await this.votes.getPastTotalSupply(blockNumber + 4)).to.be.bignumber.equal(weight[0].add(weight[1]));
  234. expect(await this.votes.getPastTotalSupply(blockNumber + 5)).to.be.bignumber.equal(weight[0]);
  235. expect(await this.votes.getPastTotalSupply(blockNumber + 6)).to.be.bignumber.equal(weight[0]);
  236. expect(await this.votes.getPastTotalSupply(blockNumber + 7)).to.be.bignumber.equal(weight[0].add(weight[2]));
  237. expect(await this.votes.getPastTotalSupply(blockNumber + 8)).to.be.bignumber.equal(weight[0].add(weight[2]));
  238. expect(await this.votes.getPastTotalSupply(blockNumber + 9)).to.be.bignumber.equal(weight[2]);
  239. expect(await this.votes.getPastTotalSupply(blockNumber + 10)).to.be.bignumber.equal(weight[2]);
  240. expect(await this.votes.getPastTotalSupply(blockNumber + 11)).to.be.bignumber.equal('0');
  241. await expectRevert(this.votes.getPastTotalSupply(blockNumber + 12), 'Checkpoints: block not yet mined');
  242. });
  243. });
  244. // The following tests are an adaptation of
  245. // https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js.
  246. describe('Compound test suite', function () {
  247. beforeEach(async function () {
  248. await this.votes.$_mint(accounts[1], tokens[0]);
  249. await this.votes.$_mint(accounts[1], tokens[1]);
  250. await this.votes.$_mint(accounts[1], tokens[2]);
  251. });
  252. describe('getPastVotes', function () {
  253. it('reverts if block number >= current block', async function () {
  254. await expectRevert(this.votes.getPastVotes(accounts[2], 5e10), 'block not yet mined');
  255. });
  256. it('returns 0 if there are no checkpoints', async function () {
  257. expect(await this.votes.getPastVotes(accounts[2], 0)).to.be.bignumber.equal('0');
  258. });
  259. it('returns the latest block if >= last checkpoint block', async function () {
  260. const tx = await this.votes.delegate(accounts[2], { from: accounts[1] });
  261. await time.advanceBlock();
  262. await time.advanceBlock();
  263. const latest = await this.votes.getVotes(accounts[2]);
  264. expect(await this.votes.getPastVotes(accounts[2], tx.receipt.blockNumber)).to.be.bignumber.equal(latest);
  265. expect(await this.votes.getPastVotes(accounts[2], tx.receipt.blockNumber + 1)).to.be.bignumber.equal(latest);
  266. });
  267. it('returns zero if < first checkpoint block', async function () {
  268. await time.advanceBlock();
  269. const tx = await this.votes.delegate(accounts[2], { from: accounts[1] });
  270. await time.advanceBlock();
  271. await time.advanceBlock();
  272. expect(await this.votes.getPastVotes(accounts[2], tx.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
  273. });
  274. });
  275. });
  276. });
  277. }
  278. module.exports = {
  279. shouldBehaveLikeVotes,
  280. };