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.
325 lines
13 KiB
325 lines
13 KiB
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,
|
|
};
|
|
|