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.
109 lines
3.8 KiB
109 lines
3.8 KiB
const { ethers } = require('hardhat');
|
|
const { expect } = require('chai');
|
|
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
|
|
|
const { getDomain, domainSeparator, Permit } = require('../../../helpers/eip712');
|
|
const time = require('../../../helpers/time');
|
|
|
|
const name = 'My Token';
|
|
const symbol = 'MTKN';
|
|
const initialSupply = 100n;
|
|
|
|
async function fixture() {
|
|
const [holder, spender, owner, other] = await ethers.getSigners();
|
|
|
|
const token = await ethers.deployContract('$ERC20Permit', [name, symbol, name]);
|
|
await token.$_mint(holder, initialSupply);
|
|
|
|
return {
|
|
holder,
|
|
spender,
|
|
owner,
|
|
other,
|
|
token,
|
|
};
|
|
}
|
|
|
|
describe('ERC20Permit', function () {
|
|
beforeEach(async function () {
|
|
Object.assign(this, await loadFixture(fixture));
|
|
});
|
|
|
|
it('initial nonce is 0', async function () {
|
|
expect(await this.token.nonces(this.holder)).to.equal(0n);
|
|
});
|
|
|
|
it('domain separator', async function () {
|
|
expect(await this.token.DOMAIN_SEPARATOR()).to.equal(await getDomain(this.token).then(domainSeparator));
|
|
});
|
|
|
|
describe('permit', function () {
|
|
const value = 42n;
|
|
const nonce = 0n;
|
|
const maxDeadline = ethers.MaxUint256;
|
|
|
|
beforeEach(function () {
|
|
this.buildData = (contract, deadline = maxDeadline) =>
|
|
getDomain(contract).then(domain => ({
|
|
domain,
|
|
types: { Permit },
|
|
message: {
|
|
owner: this.owner.address,
|
|
spender: this.spender.address,
|
|
value,
|
|
nonce,
|
|
deadline,
|
|
},
|
|
}));
|
|
});
|
|
|
|
it('accepts owner signature', async function () {
|
|
const { v, r, s } = await this.buildData(this.token)
|
|
.then(({ domain, types, message }) => this.owner.signTypedData(domain, types, message))
|
|
.then(ethers.Signature.from);
|
|
|
|
await this.token.permit(this.owner, this.spender, value, maxDeadline, v, r, s);
|
|
|
|
expect(await this.token.nonces(this.owner)).to.equal(1n);
|
|
expect(await this.token.allowance(this.owner, this.spender)).to.equal(value);
|
|
});
|
|
|
|
it('rejects reused signature', async function () {
|
|
const { v, r, s, serialized } = await this.buildData(this.token)
|
|
.then(({ domain, types, message }) => this.owner.signTypedData(domain, types, message))
|
|
.then(ethers.Signature.from);
|
|
|
|
await this.token.permit(this.owner, this.spender, value, maxDeadline, v, r, s);
|
|
|
|
const recovered = await this.buildData(this.token).then(({ domain, types, message }) =>
|
|
ethers.verifyTypedData(domain, types, { ...message, nonce: nonce + 1n, deadline: maxDeadline }, serialized),
|
|
);
|
|
|
|
await expect(this.token.permit(this.owner, this.spender, value, maxDeadline, v, r, s))
|
|
.to.be.revertedWithCustomError(this.token, 'ERC2612InvalidSigner')
|
|
.withArgs(recovered, this.owner);
|
|
});
|
|
|
|
it('rejects other signature', async function () {
|
|
const { v, r, s } = await this.buildData(this.token)
|
|
.then(({ domain, types, message }) => this.other.signTypedData(domain, types, message))
|
|
.then(ethers.Signature.from);
|
|
|
|
await expect(this.token.permit(this.owner, this.spender, value, maxDeadline, v, r, s))
|
|
.to.be.revertedWithCustomError(this.token, 'ERC2612InvalidSigner')
|
|
.withArgs(this.other, this.owner);
|
|
});
|
|
|
|
it('rejects expired permit', async function () {
|
|
const deadline = (await time.clock.timestamp()) - time.duration.weeks(1);
|
|
|
|
const { v, r, s } = await this.buildData(this.token, deadline)
|
|
.then(({ domain, types, message }) => this.owner.signTypedData(domain, types, message))
|
|
.then(ethers.Signature.from);
|
|
|
|
await expect(this.token.permit(this.owner, this.spender, value, deadline, v, r, s))
|
|
.to.be.revertedWithCustomError(this.token, 'ERC2612ExpiredSignature')
|
|
.withArgs(deadline);
|
|
});
|
|
});
|
|
});
|
|
|