|
|
|
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);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|