const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture, mine } = require('@nomicfoundation/hardhat-network-helpers'); const { bigint: time } = require('../../../helpers/time'); const { shouldBehaveLikeVotes } = require('../../../governance/utils/Votes.behavior'); const TOKENS = [ { Token: '$ERC721Votes', mode: 'blocknumber' }, // no timestamp mode for ERC721Votes yet ]; const name = 'My Vote'; const symbol = 'MTKN'; const version = '1'; const tokens = [ethers.parseEther('10000000'), 10n, 20n, 30n]; describe('ERC721Votes', function () { for (const { Token, mode } of TOKENS) { const fixture = async () => { // accounts is required by shouldBehaveLikeVotes const accounts = await ethers.getSigners(); const [holder, recipient, other1, other2] = accounts; const token = await ethers.deployContract(Token, [name, symbol, name, version]); return { accounts, holder, recipient, other1, other2, token }; }; describe(`vote with ${mode}`, function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); this.votes = this.token; }); // includes ERC6372 behavior check shouldBehaveLikeVotes(tokens, { mode, fungible: false }); describe('balanceOf', function () { beforeEach(async function () { await this.votes.$_mint(this.holder, tokens[0]); await this.votes.$_mint(this.holder, tokens[1]); await this.votes.$_mint(this.holder, tokens[2]); await this.votes.$_mint(this.holder, tokens[3]); }); it('grants to initial account', async function () { expect(await this.votes.balanceOf(this.holder)).to.equal(4n); }); }); describe('transfers', function () { beforeEach(async function () { await this.votes.$_mint(this.holder, tokens[0]); }); it('no delegation', async function () { await expect(this.votes.connect(this.holder).transferFrom(this.holder, this.recipient, tokens[0])) .to.emit(this.token, 'Transfer') .withArgs(this.holder.address, this.recipient.address, tokens[0]) .to.not.emit(this.token, 'DelegateVotesChanged'); this.holderVotes = 0n; this.recipientVotes = 0n; }); it('sender delegation', async function () { await this.votes.connect(this.holder).delegate(this.holder); const tx = await this.votes.connect(this.holder).transferFrom(this.holder, this.recipient, tokens[0]); await expect(tx) .to.emit(this.token, 'Transfer') .withArgs(this.holder.address, this.recipient.address, tokens[0]) .to.emit(this.token, 'DelegateVotesChanged') .withArgs(this.holder.address, 1n, 0n); const { logs } = await tx.wait(); const { index } = logs.find(event => event.fragment.name == 'DelegateVotesChanged'); for (const event of logs.filter(event => event.fragment.name == 'Transfer')) { expect(event.index).to.lt(index); } this.holderVotes = 0n; this.recipientVotes = 0n; }); it('receiver delegation', async function () { await this.votes.connect(this.recipient).delegate(this.recipient); const tx = await this.votes.connect(this.holder).transferFrom(this.holder, this.recipient, tokens[0]); await expect(tx) .to.emit(this.token, 'Transfer') .withArgs(this.holder.address, this.recipient.address, tokens[0]) .to.emit(this.token, 'DelegateVotesChanged') .withArgs(this.recipient.address, 0n, 1n); const { logs } = await tx.wait(); const { index } = logs.find(event => event.fragment.name == 'DelegateVotesChanged'); for (const event of logs.filter(event => event.fragment.name == 'Transfer')) { expect(event.index).to.lt(index); } this.holderVotes = 0n; this.recipientVotes = 1n; }); it('full delegation', async function () { await this.votes.connect(this.holder).delegate(this.holder); await this.votes.connect(this.recipient).delegate(this.recipient); const tx = await this.votes.connect(this.holder).transferFrom(this.holder, this.recipient, tokens[0]); await expect(tx) .to.emit(this.token, 'Transfer') .withArgs(this.holder.address, this.recipient.address, tokens[0]) .to.emit(this.token, 'DelegateVotesChanged') .withArgs(this.holder.address, 1n, 0n) .to.emit(this.token, 'DelegateVotesChanged') .withArgs(this.recipient.address, 0n, 1n); const { logs } = await tx.wait(); const { index } = logs.find(event => event.fragment.name == 'DelegateVotesChanged'); for (const event of logs.filter(event => event.fragment.name == 'Transfer')) { expect(event.index).to.lt(index); } this.holderVotes = 0; this.recipientVotes = 1n; }); it('returns the same total supply on transfers', async function () { await this.votes.connect(this.holder).delegate(this.holder); const tx = await this.votes.connect(this.holder).transferFrom(this.holder, this.recipient, tokens[0]); const timepoint = await time.clockFromReceipt[mode](tx); await mine(2); expect(await this.votes.getPastTotalSupply(timepoint - 1n)).to.equal(1n); expect(await this.votes.getPastTotalSupply(timepoint + 1n)).to.equal(1n); this.holderVotes = 0n; this.recipientVotes = 0n; }); it('generally returns the voting balance at the appropriate checkpoint', async function () { await this.votes.$_mint(this.holder, tokens[1]); await this.votes.$_mint(this.holder, tokens[2]); await this.votes.$_mint(this.holder, tokens[3]); const total = await this.votes.balanceOf(this.holder); const t1 = await this.votes.connect(this.holder).delegate(this.other1); await mine(2); const t2 = await this.votes.connect(this.holder).transferFrom(this.holder, this.other2, tokens[0]); await mine(2); const t3 = await this.votes.connect(this.holder).transferFrom(this.holder, this.other2, tokens[2]); await mine(2); const t4 = await this.votes.connect(this.other2).transferFrom(this.other2, this.holder, tokens[2]); await mine(2); 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); expect(await this.votes.getPastVotes(this.other1, t1.timepoint - 1n)).to.equal(0n); expect(await this.votes.getPastVotes(this.other1, t1.timepoint)).to.equal(total); expect(await this.votes.getPastVotes(this.other1, t1.timepoint + 1n)).to.equal(total); expect(await this.votes.getPastVotes(this.other1, t2.timepoint)).to.equal(3n); expect(await this.votes.getPastVotes(this.other1, t2.timepoint + 1n)).to.equal(3n); expect(await this.votes.getPastVotes(this.other1, t3.timepoint)).to.equal(2n); expect(await this.votes.getPastVotes(this.other1, t3.timepoint + 1n)).to.equal(2n); expect(await this.votes.getPastVotes(this.other1, t4.timepoint)).to.equal('3'); expect(await this.votes.getPastVotes(this.other1, t4.timepoint + 1n)).to.equal(3n); this.holderVotes = 0n; this.recipientVotes = 0n; }); afterEach(async function () { expect(await this.votes.getVotes(this.holder)).to.equal(this.holderVotes); expect(await this.votes.getVotes(this.recipient)).to.equal(this.recipientVotes); // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" const timepoint = await time.clock[mode](); await mine(); expect(await this.votes.getPastVotes(this.holder, timepoint)).to.equal(this.holderVotes); expect(await this.votes.getPastVotes(this.recipient, timepoint)).to.equal(this.recipientVotes); }); }); }); } });