const { ethers } = require('hardhat'); const { expect } = require('chai'); const { mine } = require('@nomicfoundation/hardhat-network-helpers'); const { getDomain, Delegation } = require('../../helpers/eip712'); const { bigint: 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.address, ethers.ZeroAddress, this.alice.address) .to.not.emit(this.votes, 'DelegateVotesChanged'); expect(await this.votes.delegates(this.alice)).to.equal(this.alice.address); }); 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.address, ethers.ZeroAddress, this.alice.address) .to.emit(this.votes, 'DelegateVotesChanged') .withArgs(this.alice.address, 0n, weight); expect(await this.votes.delegates(this.alice)).to.equal(this.alice.address); 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.address); 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.address, this.alice.address, this.bob.address) .to.emit(this.votes, 'DelegateVotesChanged') .withArgs(this.alice.address, weight, 0) .to.emit(this.votes, 'DelegateVotesChanged') .withArgs(this.bob.address, 0, weight); expect(await this.votes.delegates(this.alice)).to.equal(this.bob.address); 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.address, 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.address)).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.address, ethers.ZeroAddress, this.delegatee.address) .to.emit(this.votes, 'DelegateVotesChanged') .withArgs(this.delegatee.address, 0, weight); expect(await this.votes.delegates(this.delegator.address)).to.equal(this.delegatee.address); 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.address, 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.address); expect(args.fromDelegate).to.equal(ethers.ZeroAddress); expect(args.toDelegate).to.equal(this.other.address); }); 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.address, 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, };