123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325 |
- const { ethers } = require('hardhat');
- const { expect } = require('chai');
- const { mine } = require('@nomicfoundation/hardhat-network-helpers');
- const { getDomain, Delegation } = require('../../helpers/eip712');
- const time = require('../../helpers/time');
- const { shouldBehaveLikeERC6372 } = require('./ERC6372.behavior');
- function shouldBehaveLikeVotes(tokens, { mode = 'blocknumber', fungible = true }) {
- beforeEach(async function () {
- [this.delegator, this.delegatee, this.alice, this.bob, this.other] = this.accounts;
- this.domain = await getDomain(this.votes);
- });
- shouldBehaveLikeERC6372(mode);
- const getWeight = token => (fungible ? token : 1n);
- describe('run votes workflow', function () {
- it('initial nonce is 0', async function () {
- expect(await this.votes.nonces(this.alice)).to.equal(0n);
- });
- describe('delegation with signature', function () {
- const token = tokens[0];
- it('delegation without tokens', async function () {
- expect(await this.votes.delegates(this.alice)).to.equal(ethers.ZeroAddress);
- await expect(this.votes.connect(this.alice).delegate(this.alice))
- .to.emit(this.votes, 'DelegateChanged')
- .withArgs(this.alice, ethers.ZeroAddress, this.alice)
- .to.not.emit(this.votes, 'DelegateVotesChanged');
- expect(await this.votes.delegates(this.alice)).to.equal(this.alice);
- });
- it('delegation with tokens', async function () {
- await this.votes.$_mint(this.alice, token);
- const weight = getWeight(token);
- expect(await this.votes.delegates(this.alice)).to.equal(ethers.ZeroAddress);
- const tx = await this.votes.connect(this.alice).delegate(this.alice);
- const timepoint = await time.clockFromReceipt[mode](tx);
- await expect(tx)
- .to.emit(this.votes, 'DelegateChanged')
- .withArgs(this.alice, ethers.ZeroAddress, this.alice)
- .to.emit(this.votes, 'DelegateVotesChanged')
- .withArgs(this.alice, 0n, weight);
- expect(await this.votes.delegates(this.alice)).to.equal(this.alice);
- expect(await this.votes.getVotes(this.alice)).to.equal(weight);
- expect(await this.votes.getPastVotes(this.alice, timepoint - 1n)).to.equal(0n);
- await mine();
- expect(await this.votes.getPastVotes(this.alice, timepoint)).to.equal(weight);
- });
- it('delegation update', async function () {
- await this.votes.connect(this.alice).delegate(this.alice);
- await this.votes.$_mint(this.alice, token);
- const weight = getWeight(token);
- expect(await this.votes.delegates(this.alice)).to.equal(this.alice);
- expect(await this.votes.getVotes(this.alice)).to.equal(weight);
- expect(await this.votes.getVotes(this.bob)).to.equal(0);
- const tx = await this.votes.connect(this.alice).delegate(this.bob);
- const timepoint = await time.clockFromReceipt[mode](tx);
- await expect(tx)
- .to.emit(this.votes, 'DelegateChanged')
- .withArgs(this.alice, this.alice, this.bob)
- .to.emit(this.votes, 'DelegateVotesChanged')
- .withArgs(this.alice, weight, 0)
- .to.emit(this.votes, 'DelegateVotesChanged')
- .withArgs(this.bob, 0, weight);
- expect(await this.votes.delegates(this.alice)).to.equal(this.bob);
- expect(await this.votes.getVotes(this.alice)).to.equal(0n);
- expect(await this.votes.getVotes(this.bob)).to.equal(weight);
- expect(await this.votes.getPastVotes(this.alice, timepoint - 1n)).to.equal(weight);
- expect(await this.votes.getPastVotes(this.bob, timepoint - 1n)).to.equal(0n);
- await mine();
- expect(await this.votes.getPastVotes(this.alice, timepoint)).to.equal(0n);
- expect(await this.votes.getPastVotes(this.bob, timepoint)).to.equal(weight);
- });
- describe('with signature', function () {
- const nonce = 0n;
- it('accept signed delegation', async function () {
- await this.votes.$_mint(this.delegator, token);
- const weight = getWeight(token);
- const { r, s, v } = await this.delegator
- .signTypedData(
- this.domain,
- { Delegation },
- {
- delegatee: this.delegatee.address,
- nonce,
- expiry: ethers.MaxUint256,
- },
- )
- .then(ethers.Signature.from);
- expect(await this.votes.delegates(this.delegator)).to.equal(ethers.ZeroAddress);
- const tx = await this.votes.delegateBySig(this.delegatee, nonce, ethers.MaxUint256, v, r, s);
- const timepoint = await time.clockFromReceipt[mode](tx);
- await expect(tx)
- .to.emit(this.votes, 'DelegateChanged')
- .withArgs(this.delegator, ethers.ZeroAddress, this.delegatee)
- .to.emit(this.votes, 'DelegateVotesChanged')
- .withArgs(this.delegatee, 0, weight);
- expect(await this.votes.delegates(this.delegator.address)).to.equal(this.delegatee);
- expect(await this.votes.getVotes(this.delegator.address)).to.equal(0n);
- expect(await this.votes.getVotes(this.delegatee)).to.equal(weight);
- expect(await this.votes.getPastVotes(this.delegatee, timepoint - 1n)).to.equal(0n);
- await mine();
- expect(await this.votes.getPastVotes(this.delegatee, timepoint)).to.equal(weight);
- });
- it('rejects reused signature', async function () {
- const { r, s, v } = await this.delegator
- .signTypedData(
- this.domain,
- { Delegation },
- {
- delegatee: this.delegatee.address,
- nonce,
- expiry: ethers.MaxUint256,
- },
- )
- .then(ethers.Signature.from);
- await this.votes.delegateBySig(this.delegatee, nonce, ethers.MaxUint256, v, r, s);
- await expect(this.votes.delegateBySig(this.delegatee, nonce, ethers.MaxUint256, v, r, s))
- .to.be.revertedWithCustomError(this.votes, 'InvalidAccountNonce')
- .withArgs(this.delegator, nonce + 1n);
- });
- it('rejects bad delegatee', async function () {
- const { r, s, v } = await this.delegator
- .signTypedData(
- this.domain,
- { Delegation },
- {
- delegatee: this.delegatee.address,
- nonce,
- expiry: ethers.MaxUint256,
- },
- )
- .then(ethers.Signature.from);
- const tx = await this.votes.delegateBySig(this.other, nonce, ethers.MaxUint256, v, r, s);
- const receipt = await tx.wait();
- const [delegateChanged] = receipt.logs.filter(
- log => this.votes.interface.parseLog(log)?.name === 'DelegateChanged',
- );
- const { args } = this.votes.interface.parseLog(delegateChanged);
- expect(args.delegator).to.not.be.equal(this.delegator);
- expect(args.fromDelegate).to.equal(ethers.ZeroAddress);
- expect(args.toDelegate).to.equal(this.other);
- });
- it('rejects bad nonce', async function () {
- const { r, s, v } = await this.delegator
- .signTypedData(
- this.domain,
- { Delegation },
- {
- delegatee: this.delegatee.address,
- nonce: nonce + 1n,
- expiry: ethers.MaxUint256,
- },
- )
- .then(ethers.Signature.from);
- await expect(this.votes.delegateBySig(this.delegatee, nonce + 1n, ethers.MaxUint256, v, r, s))
- .to.be.revertedWithCustomError(this.votes, 'InvalidAccountNonce')
- .withArgs(this.delegator, 0);
- });
- it('rejects expired permit', async function () {
- const expiry = (await time.clock.timestamp()) - 1n;
- const { r, s, v } = await this.delegator
- .signTypedData(
- this.domain,
- { Delegation },
- {
- delegatee: this.delegatee.address,
- nonce,
- expiry,
- },
- )
- .then(ethers.Signature.from);
- await expect(this.votes.delegateBySig(this.delegatee, nonce, expiry, v, r, s))
- .to.be.revertedWithCustomError(this.votes, 'VotesExpiredSignature')
- .withArgs(expiry);
- });
- });
- });
- describe('getPastTotalSupply', function () {
- beforeEach(async function () {
- await this.votes.connect(this.alice).delegate(this.alice);
- });
- it('reverts if block number >= current block', async function () {
- const timepoint = 5e10;
- const clock = await this.votes.clock();
- await expect(this.votes.getPastTotalSupply(timepoint))
- .to.be.revertedWithCustomError(this.votes, 'ERC5805FutureLookup')
- .withArgs(timepoint, clock);
- });
- it('returns 0 if there are no checkpoints', async function () {
- expect(await this.votes.getPastTotalSupply(0n)).to.equal(0n);
- });
- it('returns the correct checkpointed total supply', async function () {
- const weight = tokens.map(token => getWeight(token));
- // t0 = mint #0
- const t0 = await this.votes.$_mint(this.alice, tokens[0]);
- await mine();
- // t1 = mint #1
- const t1 = await this.votes.$_mint(this.alice, tokens[1]);
- await mine();
- // t2 = burn #1
- const t2 = await this.votes.$_burn(...(fungible ? [this.alice] : []), tokens[1]);
- await mine();
- // t3 = mint #2
- const t3 = await this.votes.$_mint(this.alice, tokens[2]);
- await mine();
- // t4 = burn #0
- const t4 = await this.votes.$_burn(...(fungible ? [this.alice] : []), tokens[0]);
- await mine();
- // t5 = burn #2
- const t5 = await this.votes.$_burn(...(fungible ? [this.alice] : []), tokens[2]);
- await mine();
- t0.timepoint = await time.clockFromReceipt[mode](t0);
- t1.timepoint = await time.clockFromReceipt[mode](t1);
- t2.timepoint = await time.clockFromReceipt[mode](t2);
- t3.timepoint = await time.clockFromReceipt[mode](t3);
- t4.timepoint = await time.clockFromReceipt[mode](t4);
- t5.timepoint = await time.clockFromReceipt[mode](t5);
- expect(await this.votes.getPastTotalSupply(t0.timepoint - 1n)).to.equal(0);
- expect(await this.votes.getPastTotalSupply(t0.timepoint)).to.equal(weight[0]);
- expect(await this.votes.getPastTotalSupply(t0.timepoint + 1n)).to.equal(weight[0]);
- expect(await this.votes.getPastTotalSupply(t1.timepoint)).to.equal(weight[0] + weight[1]);
- expect(await this.votes.getPastTotalSupply(t1.timepoint + 1n)).to.equal(weight[0] + weight[1]);
- expect(await this.votes.getPastTotalSupply(t2.timepoint)).to.equal(weight[0]);
- expect(await this.votes.getPastTotalSupply(t2.timepoint + 1n)).to.equal(weight[0]);
- expect(await this.votes.getPastTotalSupply(t3.timepoint)).to.equal(weight[0] + weight[2]);
- expect(await this.votes.getPastTotalSupply(t3.timepoint + 1n)).to.equal(weight[0] + weight[2]);
- expect(await this.votes.getPastTotalSupply(t4.timepoint)).to.equal(weight[2]);
- expect(await this.votes.getPastTotalSupply(t4.timepoint + 1n)).to.equal(weight[2]);
- expect(await this.votes.getPastTotalSupply(t5.timepoint)).to.equal(0);
- await expect(this.votes.getPastTotalSupply(t5.timepoint + 1n))
- .to.be.revertedWithCustomError(this.votes, 'ERC5805FutureLookup')
- .withArgs(t5.timepoint + 1n, t5.timepoint + 1n);
- });
- });
- // The following tests are an adaptation of
- // https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js.
- describe('Compound test suite', function () {
- beforeEach(async function () {
- await this.votes.$_mint(this.alice, tokens[0]);
- await this.votes.$_mint(this.alice, tokens[1]);
- await this.votes.$_mint(this.alice, tokens[2]);
- });
- describe('getPastVotes', function () {
- it('reverts if block number >= current block', async function () {
- const clock = await this.votes.clock();
- const timepoint = 5e10; // far in the future
- await expect(this.votes.getPastVotes(this.bob, timepoint))
- .to.be.revertedWithCustomError(this.votes, 'ERC5805FutureLookup')
- .withArgs(timepoint, clock);
- });
- it('returns 0 if there are no checkpoints', async function () {
- expect(await this.votes.getPastVotes(this.bob, 0n)).to.equal(0n);
- });
- it('returns the latest block if >= last checkpoint block', async function () {
- const delegate = await this.votes.connect(this.alice).delegate(this.bob);
- const timepoint = await time.clockFromReceipt[mode](delegate);
- await mine(2);
- const latest = await this.votes.getVotes(this.bob);
- expect(await this.votes.getPastVotes(this.bob, timepoint)).to.equal(latest);
- expect(await this.votes.getPastVotes(this.bob, timepoint + 1n)).to.equal(latest);
- });
- it('returns zero if < first checkpoint block', async function () {
- await mine();
- const delegate = await this.votes.connect(this.alice).delegate(this.bob);
- const timepoint = await time.clockFromReceipt[mode](delegate);
- await mine(2);
- expect(await this.votes.getPastVotes(this.bob, timepoint - 1n)).to.equal(0n);
- });
- });
- });
- });
- }
- module.exports = {
- shouldBehaveLikeVotes,
- };
|