dexorder
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
const { expect } = require('chai');
|
||||
|
||||
const time = require('../../helpers/time');
|
||||
|
||||
function shouldBehaveLikeERC6372(mode = 'blocknumber') {
|
||||
describe('should implement ERC-6372', function () {
|
||||
beforeEach(async function () {
|
||||
this.mock = this.mock ?? this.token ?? this.votes;
|
||||
});
|
||||
|
||||
it('clock is correct', async function () {
|
||||
expect(await this.mock.clock()).to.equal(await time.clock[mode]());
|
||||
});
|
||||
|
||||
it('CLOCK_MODE is correct', async function () {
|
||||
const params = new URLSearchParams(await this.mock.CLOCK_MODE());
|
||||
expect(params.get('mode')).to.equal(mode);
|
||||
expect(params.get('from')).to.equal(mode == 'blocknumber' ? 'default' : null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shouldBehaveLikeERC6372,
|
||||
};
|
||||
@@ -0,0 +1,325 @@
|
||||
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,
|
||||
};
|
||||
102
lib_openzeppelin_contracts/test/governance/utils/Votes.test.js
Normal file
102
lib_openzeppelin_contracts/test/governance/utils/Votes.test.js
Normal file
@@ -0,0 +1,102 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { sum } = require('../../helpers/math');
|
||||
const { zip } = require('../../helpers/iterate');
|
||||
const time = require('../../helpers/time');
|
||||
|
||||
const { shouldBehaveLikeVotes } = require('./Votes.behavior');
|
||||
|
||||
const MODES = {
|
||||
blocknumber: '$VotesMock',
|
||||
timestamp: '$VotesTimestampMock',
|
||||
};
|
||||
|
||||
const AMOUNTS = [ethers.parseEther('10000000'), 10n, 20n];
|
||||
|
||||
describe('Votes', function () {
|
||||
for (const [mode, artifact] of Object.entries(MODES)) {
|
||||
const fixture = async () => {
|
||||
const accounts = await ethers.getSigners();
|
||||
|
||||
const amounts = Object.fromEntries(
|
||||
zip(
|
||||
accounts.slice(0, AMOUNTS.length).map(({ address }) => address),
|
||||
AMOUNTS,
|
||||
),
|
||||
);
|
||||
|
||||
const name = 'My Vote';
|
||||
const version = '1';
|
||||
const votes = await ethers.deployContract(artifact, [name, version]);
|
||||
|
||||
return { accounts, amounts, votes, name, version };
|
||||
};
|
||||
|
||||
describe(`vote with ${mode}`, function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
shouldBehaveLikeVotes(AMOUNTS, { mode, fungible: true });
|
||||
|
||||
it('starts with zero votes', async function () {
|
||||
expect(await this.votes.getTotalSupply()).to.equal(0n);
|
||||
});
|
||||
|
||||
describe('performs voting operations', function () {
|
||||
beforeEach(async function () {
|
||||
this.txs = [];
|
||||
for (const [account, amount] of Object.entries(this.amounts)) {
|
||||
this.txs.push(await this.votes.$_mint(account, amount));
|
||||
}
|
||||
});
|
||||
|
||||
it('reverts if block number >= current block', async function () {
|
||||
const lastTxTimepoint = await time.clockFromReceipt[mode](this.txs.at(-1));
|
||||
const clock = await this.votes.clock();
|
||||
await expect(this.votes.getPastTotalSupply(lastTxTimepoint))
|
||||
.to.be.revertedWithCustomError(this.votes, 'ERC5805FutureLookup')
|
||||
.withArgs(lastTxTimepoint, clock);
|
||||
});
|
||||
|
||||
it('delegates', async function () {
|
||||
expect(await this.votes.getVotes(this.accounts[0])).to.equal(0n);
|
||||
expect(await this.votes.getVotes(this.accounts[1])).to.equal(0n);
|
||||
expect(await this.votes.delegates(this.accounts[0])).to.equal(ethers.ZeroAddress);
|
||||
expect(await this.votes.delegates(this.accounts[1])).to.equal(ethers.ZeroAddress);
|
||||
|
||||
await this.votes.delegate(this.accounts[0], ethers.Typed.address(this.accounts[0]));
|
||||
|
||||
expect(await this.votes.getVotes(this.accounts[0])).to.equal(this.amounts[this.accounts[0].address]);
|
||||
expect(await this.votes.getVotes(this.accounts[1])).to.equal(0n);
|
||||
expect(await this.votes.delegates(this.accounts[0])).to.equal(this.accounts[0]);
|
||||
expect(await this.votes.delegates(this.accounts[1])).to.equal(ethers.ZeroAddress);
|
||||
|
||||
await this.votes.delegate(this.accounts[1], ethers.Typed.address(this.accounts[0]));
|
||||
|
||||
expect(await this.votes.getVotes(this.accounts[0])).to.equal(
|
||||
this.amounts[this.accounts[0].address] + this.amounts[this.accounts[1].address],
|
||||
);
|
||||
expect(await this.votes.getVotes(this.accounts[1])).to.equal(0n);
|
||||
expect(await this.votes.delegates(this.accounts[0])).to.equal(this.accounts[0]);
|
||||
expect(await this.votes.delegates(this.accounts[1])).to.equal(this.accounts[0]);
|
||||
});
|
||||
|
||||
it('cross delegates', async function () {
|
||||
await this.votes.delegate(this.accounts[0], ethers.Typed.address(this.accounts[1]));
|
||||
await this.votes.delegate(this.accounts[1], ethers.Typed.address(this.accounts[0]));
|
||||
|
||||
expect(await this.votes.getVotes(this.accounts[0])).to.equal(this.amounts[this.accounts[1].address]);
|
||||
expect(await this.votes.getVotes(this.accounts[1])).to.equal(this.amounts[this.accounts[0].address]);
|
||||
});
|
||||
|
||||
it('returns total amount of votes', async function () {
|
||||
const totalSupply = sum(...Object.values(this.amounts));
|
||||
expect(await this.votes.getTotalSupply()).to.equal(totalSupply);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user