mirror of openzeppelin-contracts
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
openzeppelin-contracts/test/token/ERC20/extensions/ERC20Votes.test.js

546 lines
24 KiB

const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture, mine } = require('@nomicfoundation/hardhat-network-helpers');
const { getDomain, Delegation } = require('../../../helpers/eip712');
const { batchInBlock } = require('../../../helpers/txpool');
const time = require('../../../helpers/time');
const { shouldBehaveLikeVotes } = require('../../../governance/utils/Votes.behavior');
const TOKENS = [
{ Token: '$ERC20Votes', mode: 'blocknumber' },
{ Token: '$ERC20VotesTimestampMock', mode: 'timestamp' },
];
const name = 'My Token';
const symbol = 'MTKN';
const version = '1';
const supply = ethers.parseEther('10000000');
describe('ERC20Votes', function () {
for (const { Token, mode } of TOKENS) {
const fixture = async () => {
// accounts is required by shouldBehaveLikeVotes
const accounts = await ethers.getSigners();
const [holder, recipient, delegatee, other1, other2] = accounts;
const token = await ethers.deployContract(Token, [name, symbol, name, version]);
const domain = await getDomain(token);
return { accounts, holder, recipient, delegatee, other1, other2, token, domain };
};
describe(`vote with ${mode}`, function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
this.votes = this.token;
});
// includes ERC6372 behavior check
shouldBehaveLikeVotes([1, 17, 42], { mode, fungible: true });
it('initial nonce is 0', async function () {
expect(await this.token.nonces(this.holder)).to.equal(0n);
});
it('minting restriction', async function () {
const value = 2n ** 208n;
await expect(this.token.$_mint(this.holder, value))
.to.be.revertedWithCustomError(this.token, 'ERC20ExceededSafeSupply')
.withArgs(value, value - 1n);
});
it('recent checkpoints', async function () {
await this.token.connect(this.holder).delegate(this.holder);
for (let i = 0; i < 6; i++) {
await this.token.$_mint(this.holder, 1n);
}
const timepoint = await time.clock[mode]();
expect(await this.token.numCheckpoints(this.holder)).to.equal(6n);
// recent
expect(await this.token.getPastVotes(this.holder, timepoint - 1n)).to.equal(5n);
// non-recent
expect(await this.token.getPastVotes(this.holder, timepoint - 6n)).to.equal(0n);
});
describe('set delegation', function () {
describe('call', function () {
it('delegation with balance', async function () {
await this.token.$_mint(this.holder, supply);
expect(await this.token.delegates(this.holder)).to.equal(ethers.ZeroAddress);
const tx = await this.token.connect(this.holder).delegate(this.holder);
const timepoint = await time.clockFromReceipt[mode](tx);
await expect(tx)
.to.emit(this.token, 'DelegateChanged')
.withArgs(this.holder, ethers.ZeroAddress, this.holder)
.to.emit(this.token, 'DelegateVotesChanged')
.withArgs(this.holder, 0n, supply);
expect(await this.token.delegates(this.holder)).to.equal(this.holder);
expect(await this.token.getVotes(this.holder)).to.equal(supply);
expect(await this.token.getPastVotes(this.holder, timepoint - 1n)).to.equal(0n);
await mine();
expect(await this.token.getPastVotes(this.holder, timepoint)).to.equal(supply);
});
it('delegation without balance', async function () {
expect(await this.token.delegates(this.holder)).to.equal(ethers.ZeroAddress);
await expect(this.token.connect(this.holder).delegate(this.holder))
.to.emit(this.token, 'DelegateChanged')
.withArgs(this.holder, ethers.ZeroAddress, this.holder)
.to.not.emit(this.token, 'DelegateVotesChanged');
expect(await this.token.delegates(this.holder)).to.equal(this.holder);
});
});
describe('with signature', function () {
const nonce = 0n;
beforeEach(async function () {
await this.token.$_mint(this.holder, supply);
});
it('accept signed delegation', async function () {
const { r, s, v } = await this.holder
.signTypedData(
this.domain,
{ Delegation },
{
delegatee: this.holder.address,
nonce,
expiry: ethers.MaxUint256,
},
)
.then(ethers.Signature.from);
expect(await this.token.delegates(this.holder)).to.equal(ethers.ZeroAddress);
const tx = await this.token.delegateBySig(this.holder, nonce, ethers.MaxUint256, v, r, s);
const timepoint = await time.clockFromReceipt[mode](tx);
await expect(tx)
.to.emit(this.token, 'DelegateChanged')
.withArgs(this.holder, ethers.ZeroAddress, this.holder)
.to.emit(this.token, 'DelegateVotesChanged')
.withArgs(this.holder, 0n, supply);
expect(await this.token.delegates(this.holder)).to.equal(this.holder);
expect(await this.token.getVotes(this.holder)).to.equal(supply);
expect(await this.token.getPastVotes(this.holder, timepoint - 1n)).to.equal(0n);
await mine();
expect(await this.token.getPastVotes(this.holder, timepoint)).to.equal(supply);
});
it('rejects reused signature', async function () {
const { r, s, v } = await this.holder
.signTypedData(
this.domain,
{ Delegation },
{
delegatee: this.holder.address,
nonce,
expiry: ethers.MaxUint256,
},
)
.then(ethers.Signature.from);
await this.token.delegateBySig(this.holder, nonce, ethers.MaxUint256, v, r, s);
await expect(this.token.delegateBySig(this.holder, nonce, ethers.MaxUint256, v, r, s))
.to.be.revertedWithCustomError(this.token, 'InvalidAccountNonce')
.withArgs(this.holder, nonce + 1n);
});
it('rejects bad delegatee', async function () {
const { r, s, v } = await this.holder
.signTypedData(
this.domain,
{ Delegation },
{
delegatee: this.holder.address,
nonce,
expiry: ethers.MaxUint256,
},
)
.then(ethers.Signature.from);
const tx = await this.token.delegateBySig(this.delegatee, nonce, ethers.MaxUint256, v, r, s);
const { args } = await tx
.wait()
.then(receipt => receipt.logs.find(event => event.fragment.name == 'DelegateChanged'));
expect(args[0]).to.not.equal(this.holder);
expect(args[1]).to.equal(ethers.ZeroAddress);
expect(args[2]).to.equal(this.delegatee);
});
it('rejects bad nonce', async function () {
const { r, s, v, serialized } = await this.holder
.signTypedData(
this.domain,
{ Delegation },
{
delegatee: this.holder.address,
nonce,
expiry: ethers.MaxUint256,
},
)
.then(ethers.Signature.from);
const recovered = ethers.verifyTypedData(
this.domain,
{ Delegation },
{
delegatee: this.holder.address,
nonce: nonce + 1n,
expiry: ethers.MaxUint256,
},
serialized,
);
await expect(this.token.delegateBySig(this.holder, nonce + 1n, ethers.MaxUint256, v, r, s))
.to.be.revertedWithCustomError(this.token, 'InvalidAccountNonce')
.withArgs(recovered, nonce);
});
it('rejects expired permit', async function () {
const expiry = (await time.clock.timestamp()) - time.duration.weeks(1);
const { r, s, v } = await this.holder
.signTypedData(
this.domain,
{ Delegation },
{
delegatee: this.holder.address,
nonce,
expiry,
},
)
.then(ethers.Signature.from);
await expect(this.token.delegateBySig(this.holder, nonce, expiry, v, r, s))
.to.be.revertedWithCustomError(this.token, 'VotesExpiredSignature')
.withArgs(expiry);
});
});
});
describe('change delegation', function () {
beforeEach(async function () {
await this.token.$_mint(this.holder, supply);
await this.token.connect(this.holder).delegate(this.holder);
});
it('call', async function () {
expect(await this.token.delegates(this.holder)).to.equal(this.holder);
const tx = await this.token.connect(this.holder).delegate(this.delegatee);
const timepoint = await time.clockFromReceipt[mode](tx);
await expect(tx)
.to.emit(this.token, 'DelegateChanged')
.withArgs(this.holder, this.holder, this.delegatee)
.to.emit(this.token, 'DelegateVotesChanged')
.withArgs(this.holder, supply, 0n)
.to.emit(this.token, 'DelegateVotesChanged')
.withArgs(this.delegatee, 0n, supply);
expect(await this.token.delegates(this.holder)).to.equal(this.delegatee);
expect(await this.token.getVotes(this.holder)).to.equal(0n);
expect(await this.token.getVotes(this.delegatee)).to.equal(supply);
expect(await this.token.getPastVotes(this.holder, timepoint - 1n)).to.equal(supply);
expect(await this.token.getPastVotes(this.delegatee, timepoint - 1n)).to.equal(0n);
await mine();
expect(await this.token.getPastVotes(this.holder, timepoint)).to.equal(0n);
expect(await this.token.getPastVotes(this.delegatee, timepoint)).to.equal(supply);
});
});
describe('transfers', function () {
beforeEach(async function () {
await this.token.$_mint(this.holder, supply);
});
it('no delegation', async function () {
await expect(this.token.connect(this.holder).transfer(this.recipient, 1n))
.to.emit(this.token, 'Transfer')
.withArgs(this.holder, this.recipient, 1n)
.to.not.emit(this.token, 'DelegateVotesChanged');
this.holderVotes = 0n;
this.recipientVotes = 0n;
});
it('sender delegation', async function () {
await this.token.connect(this.holder).delegate(this.holder);
const tx = await this.token.connect(this.holder).transfer(this.recipient, 1n);
await expect(tx)
.to.emit(this.token, 'Transfer')
.withArgs(this.holder, this.recipient, 1n)
.to.emit(this.token, 'DelegateVotesChanged')
.withArgs(this.holder, supply, supply - 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 = supply - 1n;
this.recipientVotes = 0n;
});
it('receiver delegation', async function () {
await this.token.connect(this.recipient).delegate(this.recipient);
const tx = await this.token.connect(this.holder).transfer(this.recipient, 1n);
await expect(tx)
.to.emit(this.token, 'Transfer')
.withArgs(this.holder, this.recipient, 1n)
.to.emit(this.token, 'DelegateVotesChanged')
.withArgs(this.recipient, 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.token.connect(this.holder).delegate(this.holder);
await this.token.connect(this.recipient).delegate(this.recipient);
const tx = await this.token.connect(this.holder).transfer(this.recipient, 1n);
await expect(tx)
.to.emit(this.token, 'Transfer')
.withArgs(this.holder, this.recipient, 1n)
.to.emit(this.token, 'DelegateVotesChanged')
.withArgs(this.holder, supply, supply - 1n)
.to.emit(this.token, 'DelegateVotesChanged')
.withArgs(this.recipient, 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 = supply - 1n;
this.recipientVotes = 1n;
});
afterEach(async function () {
expect(await this.token.getVotes(this.holder)).to.equal(this.holderVotes);
expect(await this.token.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.token.getPastVotes(this.holder, timepoint)).to.equal(this.holderVotes);
expect(await this.token.getPastVotes(this.recipient, timepoint)).to.equal(this.recipientVotes);
});
});
// The following tests are a 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.token.$_mint(this.holder, supply);
});
describe('balanceOf', function () {
it('grants to initial account', async function () {
expect(await this.token.balanceOf(this.holder)).to.equal(supply);
});
});
describe('numCheckpoints', function () {
it('returns the number of checkpoints for a delegate', async function () {
await this.token.connect(this.holder).transfer(this.recipient, 100n); //give an account a few tokens for readability
expect(await this.token.numCheckpoints(this.other1)).to.equal(0n);
const t1 = await this.token.connect(this.recipient).delegate(this.other1);
t1.timepoint = await time.clockFromReceipt[mode](t1);
expect(await this.token.numCheckpoints(this.other1)).to.equal(1n);
const t2 = await this.token.connect(this.recipient).transfer(this.other2, 10);
t2.timepoint = await time.clockFromReceipt[mode](t2);
expect(await this.token.numCheckpoints(this.other1)).to.equal(2n);
const t3 = await this.token.connect(this.recipient).transfer(this.other2, 10);
t3.timepoint = await time.clockFromReceipt[mode](t3);
expect(await this.token.numCheckpoints(this.other1)).to.equal(3n);
const t4 = await this.token.connect(this.holder).transfer(this.recipient, 20);
t4.timepoint = await time.clockFromReceipt[mode](t4);
expect(await this.token.numCheckpoints(this.other1)).to.equal(4n);
expect(await this.token.checkpoints(this.other1, 0n)).to.deep.equal([t1.timepoint, 100n]);
expect(await this.token.checkpoints(this.other1, 1n)).to.deep.equal([t2.timepoint, 90n]);
expect(await this.token.checkpoints(this.other1, 2n)).to.deep.equal([t3.timepoint, 80n]);
expect(await this.token.checkpoints(this.other1, 3n)).to.deep.equal([t4.timepoint, 100n]);
await mine();
expect(await this.token.getPastVotes(this.other1, t1.timepoint)).to.equal(100n);
expect(await this.token.getPastVotes(this.other1, t2.timepoint)).to.equal(90n);
expect(await this.token.getPastVotes(this.other1, t3.timepoint)).to.equal(80n);
expect(await this.token.getPastVotes(this.other1, t4.timepoint)).to.equal(100n);
});
it('does not add more than one checkpoint in a block', async function () {
await this.token.connect(this.holder).transfer(this.recipient, 100n);
expect(await this.token.numCheckpoints(this.other1)).to.equal(0n);
const [t1, t2, t3] = await batchInBlock([
() => this.token.connect(this.recipient).delegate(this.other1, { gasLimit: 200000 }),
() => this.token.connect(this.recipient).transfer(this.other2, 10n, { gasLimit: 200000 }),
() => this.token.connect(this.recipient).transfer(this.other2, 10n, { gasLimit: 200000 }),
]);
t1.timepoint = await time.clockFromReceipt[mode](t1);
t2.timepoint = await time.clockFromReceipt[mode](t2);
t3.timepoint = await time.clockFromReceipt[mode](t3);
expect(await this.token.numCheckpoints(this.other1)).to.equal(1);
expect(await this.token.checkpoints(this.other1, 0n)).to.be.deep.equal([t1.timepoint, 80n]);
const t4 = await this.token.connect(this.holder).transfer(this.recipient, 20n);
t4.timepoint = await time.clockFromReceipt[mode](t4);
expect(await this.token.numCheckpoints(this.other1)).to.equal(2n);
expect(await this.token.checkpoints(this.other1, 1n)).to.be.deep.equal([t4.timepoint, 100n]);
});
});
describe('getPastVotes', function () {
it('reverts if block number >= current block', async function () {
const clock = await this.token.clock();
await expect(this.token.getPastVotes(this.other1, 50_000_000_000n))
.to.be.revertedWithCustomError(this.token, 'ERC5805FutureLookup')
.withArgs(50_000_000_000n, clock);
});
it('returns 0 if there are no checkpoints', async function () {
expect(await this.token.getPastVotes(this.other1, 0n)).to.equal(0n);
});
it('returns the latest block if >= last checkpoint block', async function () {
const tx = await this.token.connect(this.holder).delegate(this.other1);
const timepoint = await time.clockFromReceipt[mode](tx);
await mine(2);
expect(await this.token.getPastVotes(this.other1, timepoint)).to.equal(supply);
expect(await this.token.getPastVotes(this.other1, timepoint + 1n)).to.equal(supply);
});
it('returns zero if < first checkpoint block', async function () {
await mine();
const tx = await this.token.connect(this.holder).delegate(this.other1);
const timepoint = await time.clockFromReceipt[mode](tx);
await mine(2);
expect(await this.token.getPastVotes(this.other1, timepoint - 1n)).to.equal(0n);
expect(await this.token.getPastVotes(this.other1, timepoint + 1n)).to.equal(supply);
});
it('generally returns the voting balance at the appropriate checkpoint', async function () {
const t1 = await this.token.connect(this.holder).delegate(this.other1);
await mine(2);
const t2 = await this.token.connect(this.holder).transfer(this.other2, 10);
await mine(2);
const t3 = await this.token.connect(this.holder).transfer(this.other2, 10);
await mine(2);
const t4 = await this.token.connect(this.other2).transfer(this.holder, 20);
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.token.getPastVotes(this.other1, t1.timepoint - 1n)).to.equal(0n);
expect(await this.token.getPastVotes(this.other1, t1.timepoint)).to.equal(supply);
expect(await this.token.getPastVotes(this.other1, t1.timepoint + 1n)).to.equal(supply);
expect(await this.token.getPastVotes(this.other1, t2.timepoint)).to.equal(supply - 10n);
expect(await this.token.getPastVotes(this.other1, t2.timepoint + 1n)).to.equal(supply - 10n);
expect(await this.token.getPastVotes(this.other1, t3.timepoint)).to.equal(supply - 20n);
expect(await this.token.getPastVotes(this.other1, t3.timepoint + 1n)).to.equal(supply - 20n);
expect(await this.token.getPastVotes(this.other1, t4.timepoint)).to.equal(supply);
expect(await this.token.getPastVotes(this.other1, t4.timepoint + 1n)).to.equal(supply);
});
});
});
describe('getPastTotalSupply', function () {
beforeEach(async function () {
await this.token.connect(this.holder).delegate(this.holder);
});
it('reverts if block number >= current block', async function () {
const clock = await this.token.clock();
await expect(this.token.getPastTotalSupply(50_000_000_000n))
.to.be.revertedWithCustomError(this.token, 'ERC5805FutureLookup')
.withArgs(50_000_000_000n, clock);
});
it('returns 0 if there are no checkpoints', async function () {
expect(await this.token.getPastTotalSupply(0n)).to.equal(0n);
});
it('returns the latest block if >= last checkpoint block', async function () {
const tx = await this.token.$_mint(this.holder, supply);
const timepoint = await time.clockFromReceipt[mode](tx);
await mine(2);
expect(await this.token.getPastTotalSupply(timepoint)).to.equal(supply);
expect(await this.token.getPastTotalSupply(timepoint + 1n)).to.equal(supply);
});
it('returns zero if < first checkpoint block', async function () {
await mine();
const tx = await this.token.$_mint(this.holder, supply);
const timepoint = await time.clockFromReceipt[mode](tx);
await mine(2);
expect(await this.token.getPastTotalSupply(timepoint - 1n)).to.equal(0n);
expect(await this.token.getPastTotalSupply(timepoint + 1n)).to.equal(supply);
});
it('generally returns the voting balance at the appropriate checkpoint', async function () {
const t1 = await this.token.$_mint(this.holder, supply);
await mine(2);
const t2 = await this.token.$_burn(this.holder, 10n);
await mine(2);
const t3 = await this.token.$_burn(this.holder, 10n);
await mine(2);
const t4 = await this.token.$_mint(this.holder, 20n);
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.token.getPastTotalSupply(t1.timepoint - 1n)).to.equal(0n);
expect(await this.token.getPastTotalSupply(t1.timepoint)).to.equal(supply);
expect(await this.token.getPastTotalSupply(t1.timepoint + 1n)).to.equal(supply);
expect(await this.token.getPastTotalSupply(t2.timepoint)).to.equal(supply - 10n);
expect(await this.token.getPastTotalSupply(t2.timepoint + 1n)).to.equal(supply - 10n);
expect(await this.token.getPastTotalSupply(t3.timepoint)).to.equal(supply - 20n);
expect(await this.token.getPastTotalSupply(t3.timepoint + 1n)).to.equal(supply - 20n);
expect(await this.token.getPastTotalSupply(t4.timepoint)).to.equal(supply);
expect(await this.token.getPastTotalSupply(t4.timepoint + 1n)).to.equal(supply);
});
});
});
}
});