Migrate ERC20 and ERC20Wrapper tests to ethersjs (#4743)

Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
pull/4746/head^2
Renan Souza 1 year ago committed by GitHub
parent a32077bbac
commit c35057978f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 326
      test/token/ERC20/ERC20.behavior.js
  2. 195
      test/token/ERC20/ERC20.test.js
  3. 267
      test/token/ERC20/extensions/ERC20Wrapper.test.js

@ -1,335 +1,271 @@
const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers');
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { ZERO_ADDRESS, MAX_UINT256 } = constants;
const { expectRevertCustomError } = require('../../helpers/customError');
function shouldBehaveLikeERC20(initialSupply, accounts, opts = {}) {
const [initialHolder, recipient, anotherAccount] = accounts;
function shouldBehaveLikeERC20(initialSupply, opts = {}) {
const { forcedApproval } = opts;
describe('total supply', function () {
it('returns the total token value', async function () {
expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply);
});
it('total supply: returns the total token value', async function () {
expect(await this.token.totalSupply()).to.equal(initialSupply);
});
describe('balanceOf', function () {
describe('when the requested account has no tokens', function () {
it('returns zero', async function () {
expect(await this.token.balanceOf(anotherAccount)).to.be.bignumber.equal('0');
});
it('returns zero when the requested account has no tokens', async function () {
expect(await this.token.balanceOf(this.anotherAccount)).to.equal(0n);
});
describe('when the requested account has some tokens', function () {
it('returns the total token value', async function () {
expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(initialSupply);
});
it('returns the total token value when the requested account has some tokens', async function () {
expect(await this.token.balanceOf(this.initialHolder)).to.equal(initialSupply);
});
});
describe('transfer', function () {
shouldBehaveLikeERC20Transfer(initialHolder, recipient, initialSupply, function (from, to, value) {
return this.token.transfer(to, value, { from });
beforeEach(function () {
this.transfer = (from, to, value) => this.token.connect(from).transfer(to, value);
});
shouldBehaveLikeERC20Transfer(initialSupply);
});
describe('transfer from', function () {
const spender = recipient;
describe('when the token owner is not the zero address', function () {
const tokenOwner = initialHolder;
describe('when the recipient is not the zero address', function () {
const to = anotherAccount;
describe('when the spender has enough allowance', function () {
beforeEach(async function () {
await this.token.approve(spender, initialSupply, { from: initialHolder });
await this.token.connect(this.initialHolder).approve(this.recipient, initialSupply);
});
describe('when the token owner has enough balance', function () {
const value = initialSupply;
it('transfers the requested value', async function () {
await this.token.transferFrom(tokenOwner, to, value, { from: spender });
expect(await this.token.balanceOf(tokenOwner)).to.be.bignumber.equal('0');
beforeEach(async function () {
this.tx = await this.token
.connect(this.recipient)
.transferFrom(this.initialHolder, this.anotherAccount, value);
});
expect(await this.token.balanceOf(to)).to.be.bignumber.equal(value);
it('transfers the requested value', async function () {
await expect(this.tx).to.changeTokenBalances(
this.token,
[this.initialHolder, this.anotherAccount],
[-value, value],
);
});
it('decreases the spender allowance', async function () {
await this.token.transferFrom(tokenOwner, to, value, { from: spender });
expect(await this.token.allowance(tokenOwner, spender)).to.be.bignumber.equal('0');
expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(0n);
});
it('emits a transfer event', async function () {
expectEvent(await this.token.transferFrom(tokenOwner, to, value, { from: spender }), 'Transfer', {
from: tokenOwner,
to: to,
value: value,
});
await expect(this.tx)
.to.emit(this.token, 'Transfer')
.withArgs(this.initialHolder.address, this.anotherAccount.address, value);
});
if (forcedApproval) {
it('emits an approval event', async function () {
expectEvent(await this.token.transferFrom(tokenOwner, to, value, { from: spender }), 'Approval', {
owner: tokenOwner,
spender: spender,
value: await this.token.allowance(tokenOwner, spender),
});
await expect(this.tx)
.to.emit(this.token, 'Approval')
.withArgs(
this.initialHolder.address,
this.recipient.address,
await this.token.allowance(this.initialHolder, this.recipient),
);
});
} else {
it('does not emit an approval event', async function () {
expectEvent.notEmitted(
await this.token.transferFrom(tokenOwner, to, value, { from: spender }),
'Approval',
);
await expect(this.tx).to.not.emit(this.token, 'Approval');
});
}
});
describe('when the token owner does not have enough balance', function () {
it('reverts when the token owner does not have enough balance', async function () {
const value = initialSupply;
beforeEach('reducing balance', async function () {
await this.token.transfer(to, 1, { from: tokenOwner });
});
it('reverts', async function () {
await expectRevertCustomError(
this.token.transferFrom(tokenOwner, to, value, { from: spender }),
'ERC20InsufficientBalance',
[tokenOwner, value - 1, value],
);
});
await this.token.connect(this.initialHolder).transfer(this.anotherAccount, 1n);
await expect(
this.token.connect(this.recipient).transferFrom(this.initialHolder, this.anotherAccount, value),
)
.to.revertedWithCustomError(this.token, 'ERC20InsufficientBalance')
.withArgs(this.initialHolder.address, value - 1n, value);
});
});
describe('when the spender does not have enough allowance', function () {
const allowance = initialSupply.subn(1);
const allowance = initialSupply - 1n;
beforeEach(async function () {
await this.token.approve(spender, allowance, { from: tokenOwner });
await this.token.connect(this.initialHolder).approve(this.recipient, allowance);
});
describe('when the token owner has enough balance', function () {
it('reverts when the token owner has enough balance', async function () {
const value = initialSupply;
it('reverts', async function () {
await expectRevertCustomError(
this.token.transferFrom(tokenOwner, to, value, { from: spender }),
'ERC20InsufficientAllowance',
[spender, allowance, value],
);
});
await expect(
this.token.connect(this.recipient).transferFrom(this.initialHolder, this.anotherAccount, value),
)
.to.be.revertedWithCustomError(this.token, 'ERC20InsufficientAllowance')
.withArgs(this.recipient.address, allowance, value);
});
describe('when the token owner does not have enough balance', function () {
it('reverts when the token owner does not have enough balance', async function () {
const value = allowance;
beforeEach('reducing balance', async function () {
await this.token.transfer(to, 2, { from: tokenOwner });
});
it('reverts', async function () {
await expectRevertCustomError(
this.token.transferFrom(tokenOwner, to, value, { from: spender }),
'ERC20InsufficientBalance',
[tokenOwner, value - 1, value],
);
});
await this.token.connect(this.initialHolder).transfer(this.anotherAccount, 2);
await expect(
this.token.connect(this.recipient).transferFrom(this.initialHolder, this.anotherAccount, value),
)
.to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance')
.withArgs(this.initialHolder.address, value - 1n, value);
});
});
describe('when the spender has unlimited allowance', function () {
beforeEach(async function () {
await this.token.approve(spender, MAX_UINT256, { from: initialHolder });
await this.token.connect(this.initialHolder).approve(this.recipient, ethers.MaxUint256);
this.tx = await this.token
.connect(this.recipient)
.transferFrom(this.initialHolder, this.anotherAccount, 1n);
});
it('does not decrease the spender allowance', async function () {
await this.token.transferFrom(tokenOwner, to, 1, { from: spender });
expect(await this.token.allowance(tokenOwner, spender)).to.be.bignumber.equal(MAX_UINT256);
expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(ethers.MaxUint256);
});
it('does not emit an approval event', async function () {
expectEvent.notEmitted(await this.token.transferFrom(tokenOwner, to, 1, { from: spender }), 'Approval');
await expect(this.tx).to.not.emit(this.token, 'Approval');
});
});
});
describe('when the recipient is the zero address', function () {
it('reverts when the recipient is the zero address', async function () {
const value = initialSupply;
const to = ZERO_ADDRESS;
beforeEach(async function () {
await this.token.approve(spender, value, { from: tokenOwner });
});
it('reverts', async function () {
await expectRevertCustomError(
this.token.transferFrom(tokenOwner, to, value, { from: spender }),
'ERC20InvalidReceiver',
[ZERO_ADDRESS],
);
});
await this.token.connect(this.initialHolder).approve(this.recipient, value);
await expect(this.token.connect(this.recipient).transferFrom(this.initialHolder, ethers.ZeroAddress, value))
.to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver')
.withArgs(ethers.ZeroAddress);
});
});
describe('when the token owner is the zero address', function () {
const value = 0;
const tokenOwner = ZERO_ADDRESS;
const to = recipient;
it('reverts', async function () {
await expectRevertCustomError(
this.token.transferFrom(tokenOwner, to, value, { from: spender }),
'ERC20InvalidApprover',
[ZERO_ADDRESS],
);
});
it('reverts when the token owner is the zero address', async function () {
const value = 0n;
await expect(this.token.connect(this.recipient).transferFrom(ethers.ZeroAddress, this.recipient, value))
.to.be.revertedWithCustomError(this.token, 'ERC20InvalidApprover')
.withArgs(ethers.ZeroAddress);
});
});
describe('approve', function () {
shouldBehaveLikeERC20Approve(initialHolder, recipient, initialSupply, function (owner, spender, value) {
return this.token.approve(spender, value, { from: owner });
beforeEach(function () {
this.approve = (owner, spender, value) => this.token.connect(owner).approve(spender, value);
});
shouldBehaveLikeERC20Approve(initialSupply);
});
}
function shouldBehaveLikeERC20Transfer(from, to, balance, transfer) {
function shouldBehaveLikeERC20Transfer(balance) {
describe('when the recipient is not the zero address', function () {
describe('when the sender does not have enough balance', function () {
const value = balance.addn(1);
it('reverts', async function () {
await expectRevertCustomError(transfer.call(this, from, to, value), 'ERC20InsufficientBalance', [
from,
balance,
value,
]);
});
it('reverts when the sender does not have enough balance', async function () {
const value = balance + 1n;
await expect(this.transfer(this.initialHolder, this.recipient, value))
.to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance')
.withArgs(this.initialHolder.address, balance, value);
});
describe('when the sender transfers all balance', function () {
const value = balance;
it('transfers the requested value', async function () {
await transfer.call(this, from, to, value);
expect(await this.token.balanceOf(from)).to.be.bignumber.equal('0');
beforeEach(async function () {
this.tx = await this.transfer(this.initialHolder, this.recipient, value);
});
expect(await this.token.balanceOf(to)).to.be.bignumber.equal(value);
it('transfers the requested value', async function () {
await expect(this.tx).to.changeTokenBalances(this.token, [this.initialHolder, this.recipient], [-value, value]);
});
it('emits a transfer event', async function () {
expectEvent(await transfer.call(this, from, to, value), 'Transfer', { from, to, value: value });
await expect(this.tx)
.to.emit(this.token, 'Transfer')
.withArgs(this.initialHolder.address, this.recipient.address, value);
});
});
describe('when the sender transfers zero tokens', function () {
const value = new BN('0');
it('transfers the requested value', async function () {
await transfer.call(this, from, to, value);
const value = 0n;
expect(await this.token.balanceOf(from)).to.be.bignumber.equal(balance);
beforeEach(async function () {
this.tx = await this.transfer(this.initialHolder, this.recipient, value);
});
expect(await this.token.balanceOf(to)).to.be.bignumber.equal('0');
it('transfers the requested value', async function () {
await expect(this.tx).to.changeTokenBalances(this.token, [this.initialHolder, this.recipient], [0n, 0n]);
});
it('emits a transfer event', async function () {
expectEvent(await transfer.call(this, from, to, value), 'Transfer', { from, to, value: value });
await expect(this.tx)
.to.emit(this.token, 'Transfer')
.withArgs(this.initialHolder.address, this.recipient.address, value);
});
});
});
describe('when the recipient is the zero address', function () {
it('reverts', async function () {
await expectRevertCustomError(transfer.call(this, from, ZERO_ADDRESS, balance), 'ERC20InvalidReceiver', [
ZERO_ADDRESS,
]);
});
it('reverts when the recipient is the zero address', async function () {
await expect(this.transfer(this.initialHolder, ethers.ZeroAddress, balance))
.to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver')
.withArgs(ethers.ZeroAddress);
});
}
function shouldBehaveLikeERC20Approve(owner, spender, supply, approve) {
function shouldBehaveLikeERC20Approve(supply) {
describe('when the spender is not the zero address', function () {
describe('when the sender has enough balance', function () {
const value = supply;
it('emits an approval event', async function () {
expectEvent(await approve.call(this, owner, spender, value), 'Approval', {
owner: owner,
spender: spender,
value: value,
});
await expect(this.approve(this.initialHolder, this.recipient, value))
.to.emit(this.token, 'Approval')
.withArgs(this.initialHolder.address, this.recipient.address, value);
});
describe('when there was no approved value before', function () {
it('approves the requested value', async function () {
await approve.call(this, owner, spender, value);
it('approves the requested value when there was no approved value before', async function () {
await this.approve(this.initialHolder, this.recipient, value);
expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(value);
});
expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(value);
});
describe('when the spender had an approved value', function () {
beforeEach(async function () {
await approve.call(this, owner, spender, new BN(1));
});
it('approves the requested value and replaces the previous one', async function () {
await approve.call(this, owner, spender, value);
it('approves the requested value and replaces the previous one when the spender had an approved value', async function () {
await this.approve(this.initialHolder, this.recipient, 1n);
await this.approve(this.initialHolder, this.recipient, value);
expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(value);
});
expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(value);
});
});
describe('when the sender does not have enough balance', function () {
const value = supply.addn(1);
const value = supply + 1n;
it('emits an approval event', async function () {
expectEvent(await approve.call(this, owner, spender, value), 'Approval', {
owner: owner,
spender: spender,
value: value,
});
await expect(this.approve(this.initialHolder, this.recipient, value))
.to.emit(this.token, 'Approval')
.withArgs(this.initialHolder.address, this.recipient.address, value);
});
describe('when there was no approved value before', function () {
it('approves the requested value', async function () {
await approve.call(this, owner, spender, value);
it('approves the requested value when there was no approved value before', async function () {
await this.approve(this.initialHolder, this.recipient, value);
expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(value);
});
expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(value);
});
describe('when the spender had an approved value', function () {
beforeEach(async function () {
await approve.call(this, owner, spender, new BN(1));
});
it('approves the requested value and replaces the previous one when the spender had an approved value', async function () {
await this.approve(this.initialHolder, this.recipient, 1n);
await this.approve(this.initialHolder, this.recipient, value);
it('approves the requested value and replaces the previous one', async function () {
await approve.call(this, owner, spender, value);
expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(value);
});
expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(value);
});
});
});
describe('when the spender is the zero address', function () {
it('reverts', async function () {
await expectRevertCustomError(approve.call(this, owner, ZERO_ADDRESS, supply), `ERC20InvalidSpender`, [
ZERO_ADDRESS,
]);
});
it('reverts when the spender is the zero address', async function () {
await expect(this.approve(this.initialHolder, ethers.ZeroAddress, supply))
.to.be.revertedWithCustomError(this.token, `ERC20InvalidSpender`)
.withArgs(ethers.ZeroAddress);
});
}

@ -1,34 +1,37 @@
const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { ZERO_ADDRESS } = constants;
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');
const {
shouldBehaveLikeERC20,
shouldBehaveLikeERC20Transfer,
shouldBehaveLikeERC20Approve,
} = require('./ERC20.behavior');
const { expectRevertCustomError } = require('../../helpers/customError');
const TOKENS = [
{ Token: artifacts.require('$ERC20') },
{ Token: artifacts.require('$ERC20ApprovalMock'), forcedApproval: true },
];
const TOKENS = [{ Token: '$ERC20' }, { Token: '$ERC20ApprovalMock', forcedApproval: true }];
contract('ERC20', function (accounts) {
const [initialHolder, recipient] = accounts;
const name = 'My Token';
const symbol = 'MTKN';
const initialSupply = new BN(100);
const name = 'My Token';
const symbol = 'MTKN';
const initialSupply = 100n;
describe('ERC20', function () {
for (const { Token, forcedApproval } of TOKENS) {
describe(`using ${Token._json.contractName}`, function () {
describe(Token, function () {
const fixture = async () => {
const [initialHolder, recipient, anotherAccount] = await ethers.getSigners();
const token = await ethers.deployContract(Token, [name, symbol]);
await token.$_mint(initialHolder, initialSupply);
return { initialHolder, recipient, anotherAccount, token };
};
beforeEach(async function () {
this.token = await Token.new(name, symbol);
await this.token.$_mint(initialHolder, initialSupply);
Object.assign(this, await loadFixture(fixture));
});
shouldBehaveLikeERC20(initialSupply, accounts, { forcedApproval });
shouldBehaveLikeERC20(initialSupply, { forcedApproval });
it('has a name', async function () {
expect(await this.token.name()).to.equal(name);
@ -39,162 +42,164 @@ contract('ERC20', function (accounts) {
});
it('has 18 decimals', async function () {
expect(await this.token.decimals()).to.be.bignumber.equal('18');
expect(await this.token.decimals()).to.equal(18n);
});
describe('_mint', function () {
const value = new BN(50);
const value = 50n;
it('rejects a null account', async function () {
await expectRevertCustomError(this.token.$_mint(ZERO_ADDRESS, value), 'ERC20InvalidReceiver', [ZERO_ADDRESS]);
await expect(this.token.$_mint(ethers.ZeroAddress, value))
.to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver')
.withArgs(ethers.ZeroAddress);
});
it('rejects overflow', async function () {
const maxUint256 = new BN('2').pow(new BN(256)).subn(1);
await expectRevert(
this.token.$_mint(recipient, maxUint256),
'reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)',
await expect(this.token.$_mint(this.recipient, ethers.MaxUint256)).to.be.revertedWithPanic(
PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW,
);
});
describe('for a non zero account', function () {
beforeEach('minting', async function () {
this.receipt = await this.token.$_mint(recipient, value);
this.tx = await this.token.$_mint(this.recipient, value);
});
it('increments totalSupply', async function () {
const expectedSupply = initialSupply.add(value);
expect(await this.token.totalSupply()).to.be.bignumber.equal(expectedSupply);
await expect(await this.token.totalSupply()).to.equal(initialSupply + value);
});
it('increments recipient balance', async function () {
expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(value);
await expect(this.tx).to.changeTokenBalance(this.token, this.recipient, value);
});
it('emits Transfer event', async function () {
const event = expectEvent(this.receipt, 'Transfer', { from: ZERO_ADDRESS, to: recipient });
expect(event.args.value).to.be.bignumber.equal(value);
await expect(this.tx)
.to.emit(this.token, 'Transfer')
.withArgs(ethers.ZeroAddress, this.recipient.address, value);
});
});
});
describe('_burn', function () {
it('rejects a null account', async function () {
await expectRevertCustomError(this.token.$_burn(ZERO_ADDRESS, new BN(1)), 'ERC20InvalidSender', [
ZERO_ADDRESS,
]);
await expect(this.token.$_burn(ethers.ZeroAddress, 1n))
.to.be.revertedWithCustomError(this.token, 'ERC20InvalidSender')
.withArgs(ethers.ZeroAddress);
});
describe('for a non zero account', function () {
it('rejects burning more than balance', async function () {
await expectRevertCustomError(
this.token.$_burn(initialHolder, initialSupply.addn(1)),
'ERC20InsufficientBalance',
[initialHolder, initialSupply, initialSupply.addn(1)],
);
await expect(this.token.$_burn(this.initialHolder, initialSupply + 1n))
.to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance')
.withArgs(this.initialHolder.address, initialSupply, initialSupply + 1n);
});
const describeBurn = function (description, value) {
describe(description, function () {
beforeEach('burning', async function () {
this.receipt = await this.token.$_burn(initialHolder, value);
this.tx = await this.token.$_burn(this.initialHolder, value);
});
it('decrements totalSupply', async function () {
const expectedSupply = initialSupply.sub(value);
expect(await this.token.totalSupply()).to.be.bignumber.equal(expectedSupply);
expect(await this.token.totalSupply()).to.equal(initialSupply - value);
});
it('decrements initialHolder balance', async function () {
const expectedBalance = initialSupply.sub(value);
expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(expectedBalance);
await expect(this.tx).to.changeTokenBalance(this.token, this.initialHolder, -value);
});
it('emits Transfer event', async function () {
const event = expectEvent(this.receipt, 'Transfer', { from: initialHolder, to: ZERO_ADDRESS });
expect(event.args.value).to.be.bignumber.equal(value);
await expect(this.tx)
.to.emit(this.token, 'Transfer')
.withArgs(this.initialHolder.address, ethers.ZeroAddress, value);
});
});
};
describeBurn('for entire balance', initialSupply);
describeBurn('for less value than balance', initialSupply.subn(1));
describeBurn('for less value than balance', initialSupply - 1n);
});
});
describe('_update', function () {
const value = new BN(1);
const value = 1n;
beforeEach(async function () {
this.totalSupply = await this.token.totalSupply();
});
it('from is the zero address', async function () {
const balanceBefore = await this.token.balanceOf(initialHolder);
const totalSupply = await this.token.totalSupply();
const tx = await this.token.$_update(ethers.ZeroAddress, this.initialHolder, value);
await expect(tx)
.to.emit(this.token, 'Transfer')
.withArgs(ethers.ZeroAddress, this.initialHolder.address, value);
expectEvent(await this.token.$_update(ZERO_ADDRESS, initialHolder, value), 'Transfer', {
from: ZERO_ADDRESS,
to: initialHolder,
value: value,
});
expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply.add(value));
expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(balanceBefore.add(value));
expect(await this.token.totalSupply()).to.equal(this.totalSupply + value);
await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, value);
});
it('to is the zero address', async function () {
const balanceBefore = await this.token.balanceOf(initialHolder);
const totalSupply = await this.token.totalSupply();
const tx = await this.token.$_update(this.initialHolder, ethers.ZeroAddress, value);
await expect(tx)
.to.emit(this.token, 'Transfer')
.withArgs(this.initialHolder.address, ethers.ZeroAddress, value);
expectEvent(await this.token.$_update(initialHolder, ZERO_ADDRESS, value), 'Transfer', {
from: initialHolder,
to: ZERO_ADDRESS,
value: value,
});
expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply.sub(value));
expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(balanceBefore.sub(value));
expect(await this.token.totalSupply()).to.equal(this.totalSupply - value);
await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, -value);
});
it('from and to are the zero address', async function () {
const totalSupply = await this.token.totalSupply();
describe('from and to are the same address', function () {
it('zero address', async function () {
const tx = await this.token.$_update(ethers.ZeroAddress, ethers.ZeroAddress, value);
await expect(tx).to.emit(this.token, 'Transfer').withArgs(ethers.ZeroAddress, ethers.ZeroAddress, value);
await this.token.$_update(ZERO_ADDRESS, ZERO_ADDRESS, value);
expect(await this.token.totalSupply()).to.equal(this.totalSupply);
await expect(tx).to.changeTokenBalance(this.token, ethers.ZeroAddress, 0n);
});
expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply);
expectEvent(await this.token.$_update(ZERO_ADDRESS, ZERO_ADDRESS, value), 'Transfer', {
from: ZERO_ADDRESS,
to: ZERO_ADDRESS,
value: value,
describe('non zero address', function () {
it('reverts without balance', async function () {
await expect(this.token.$_update(this.recipient, this.recipient, value))
.to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance')
.withArgs(this.recipient.address, 0n, value);
});
it('executes with balance', async function () {
const tx = await this.token.$_update(this.initialHolder, this.initialHolder, value);
await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, 0n);
await expect(tx)
.to.emit(this.token, 'Transfer')
.withArgs(this.initialHolder.address, this.initialHolder.address, value);
});
});
});
});
describe('_transfer', function () {
shouldBehaveLikeERC20Transfer(initialHolder, recipient, initialSupply, function (from, to, value) {
return this.token.$_transfer(from, to, value);
beforeEach(function () {
this.transfer = this.token.$_transfer;
});
describe('when the sender is the zero address', function () {
it('reverts', async function () {
await expectRevertCustomError(
this.token.$_transfer(ZERO_ADDRESS, recipient, initialSupply),
'ERC20InvalidSender',
[ZERO_ADDRESS],
);
});
shouldBehaveLikeERC20Transfer(initialSupply);
it('reverts when the sender is the zero address', async function () {
await expect(this.token.$_transfer(ethers.ZeroAddress, this.recipient, initialSupply))
.to.be.revertedWithCustomError(this.token, 'ERC20InvalidSender')
.withArgs(ethers.ZeroAddress);
});
});
describe('_approve', function () {
shouldBehaveLikeERC20Approve(initialHolder, recipient, initialSupply, function (owner, spender, value) {
return this.token.$_approve(owner, spender, value);
beforeEach(function () {
this.approve = this.token.$_approve;
});
describe('when the owner is the zero address', function () {
it('reverts', async function () {
await expectRevertCustomError(
this.token.$_approve(ZERO_ADDRESS, recipient, initialSupply),
'ERC20InvalidApprover',
[ZERO_ADDRESS],
);
});
shouldBehaveLikeERC20Approve(initialSupply);
it('reverts when the owner is the zero address', async function () {
await expect(this.token.$_approve(ethers.ZeroAddress, this.recipient, initialSupply))
.to.be.revertedWithCustomError(this.token, 'ERC20InvalidApprover')
.withArgs(ethers.ZeroAddress);
});
});
});

@ -1,31 +1,34 @@
const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers');
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { ZERO_ADDRESS, MAX_UINT256 } = constants;
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { shouldBehaveLikeERC20 } = require('../ERC20.behavior');
const { expectRevertCustomError } = require('../../../helpers/customError');
const NotAnERC20 = artifacts.require('CallReceiverMock');
const ERC20Decimals = artifacts.require('$ERC20DecimalsMock');
const ERC20Wrapper = artifacts.require('$ERC20Wrapper');
const name = 'My Token';
const symbol = 'MTKN';
const initialSupply = 100n;
contract('ERC20Wrapper', function (accounts) {
const [initialHolder, receiver] = accounts;
async function fixture() {
const [initialHolder, recipient, anotherAccount] = await ethers.getSigners();
const underlying = await ethers.deployContract('$ERC20DecimalsMock', [name, symbol, 9]);
await underlying.$_mint(initialHolder, initialSupply);
const token = await ethers.deployContract('$ERC20Wrapper', [`Wrapped ${name}`, `W${symbol}`, underlying]);
return { initialHolder, recipient, anotherAccount, underlying, token };
}
describe('ERC20Wrapper', function () {
const name = 'My Token';
const symbol = 'MTKN';
const initialSupply = new BN(100);
beforeEach(async function () {
this.underlying = await ERC20Decimals.new(name, symbol, 9);
await this.underlying.$_mint(initialHolder, initialSupply);
this.token = await ERC20Wrapper.new(`Wrapped ${name}`, `W${symbol}`, this.underlying.address);
Object.assign(this, await loadFixture(fixture));
});
afterEach(async function () {
expect(await this.underlying.balanceOf(this.token.address)).to.be.bignumber.equal(await this.token.totalSupply());
afterEach('Underlying balance', async function () {
expect(await this.underlying.balanceOf(this.token)).to.be.equal(await this.token.totalSupply());
});
it('has a name', async function () {
@ -37,175 +40,169 @@ contract('ERC20Wrapper', function (accounts) {
});
it('has the same decimals as the underlying token', async function () {
expect(await this.token.decimals()).to.be.bignumber.equal('9');
expect(await this.token.decimals()).to.be.equal(9n);
});
it('decimals default back to 18 if token has no metadata', async function () {
const noDecimals = await NotAnERC20.new();
const otherToken = await ERC20Wrapper.new(`Wrapped ${name}`, `W${symbol}`, noDecimals.address);
expect(await otherToken.decimals()).to.be.bignumber.equal('18');
const noDecimals = await ethers.deployContract('CallReceiverMock');
const token = await ethers.deployContract('$ERC20Wrapper', [`Wrapped ${name}`, `W${symbol}`, noDecimals]);
expect(await token.decimals()).to.be.equal(18n);
});
it('has underlying', async function () {
expect(await this.token.underlying()).to.be.bignumber.equal(this.underlying.address);
expect(await this.token.underlying()).to.be.equal(this.underlying.target);
});
describe('deposit', function () {
it('valid', async function () {
await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder });
const { tx } = await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder });
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
from: initialHolder,
to: this.token.address,
value: initialSupply,
});
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
from: ZERO_ADDRESS,
to: initialHolder,
value: initialSupply,
});
it('executes with approval', async function () {
await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply);
const tx = await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply);
await expect(tx)
.to.emit(this.underlying, 'Transfer')
.withArgs(this.initialHolder.address, this.token.target, initialSupply)
.to.emit(this.token, 'Transfer')
.withArgs(ethers.ZeroAddress, this.initialHolder.address, initialSupply);
await expect(tx).to.changeTokenBalances(
this.underlying,
[this.initialHolder, this.token],
[-initialSupply, initialSupply],
);
await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, initialSupply);
});
it('missing approval', async function () {
await expectRevertCustomError(
this.token.depositFor(initialHolder, initialSupply, { from: initialHolder }),
'ERC20InsufficientAllowance',
[this.token.address, 0, initialSupply],
);
it('reverts when missing approval', async function () {
await expect(this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply))
.to.be.revertedWithCustomError(this.underlying, 'ERC20InsufficientAllowance')
.withArgs(this.token.target, 0, initialSupply);
});
it('missing balance', async function () {
await this.underlying.approve(this.token.address, MAX_UINT256, { from: initialHolder });
await expectRevertCustomError(
this.token.depositFor(initialHolder, MAX_UINT256, { from: initialHolder }),
'ERC20InsufficientBalance',
[initialHolder, initialSupply, MAX_UINT256],
);
it('reverts when inssuficient balance', async function () {
await this.underlying.connect(this.initialHolder).approve(this.token, ethers.MaxUint256);
await expect(this.token.connect(this.initialHolder).depositFor(this.initialHolder, ethers.MaxUint256))
.to.be.revertedWithCustomError(this.underlying, 'ERC20InsufficientBalance')
.withArgs(this.initialHolder.address, initialSupply, ethers.MaxUint256);
});
it('to other account', async function () {
await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder });
const { tx } = await this.token.depositFor(receiver, initialSupply, { from: initialHolder });
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
from: initialHolder,
to: this.token.address,
value: initialSupply,
});
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
from: ZERO_ADDRESS,
to: receiver,
value: initialSupply,
});
it('deposits to other account', async function () {
await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply);
const tx = await this.token.connect(this.initialHolder).depositFor(this.recipient, initialSupply);
await expect(tx)
.to.emit(this.underlying, 'Transfer')
.withArgs(this.initialHolder.address, this.token.target, initialSupply)
.to.emit(this.token, 'Transfer')
.withArgs(ethers.ZeroAddress, this.recipient.address, initialSupply);
await expect(tx).to.changeTokenBalances(
this.underlying,
[this.initialHolder, this.token],
[-initialSupply, initialSupply],
);
await expect(tx).to.changeTokenBalances(this.token, [this.initialHolder, this.recipient], [0, initialSupply]);
});
it('reverts minting to the wrapper contract', async function () {
await this.underlying.approve(this.token.address, MAX_UINT256, { from: initialHolder });
await expectRevertCustomError(
this.token.depositFor(this.token.address, MAX_UINT256, { from: initialHolder }),
'ERC20InvalidReceiver',
[this.token.address],
);
await this.underlying.connect(this.initialHolder).approve(this.token, ethers.MaxUint256);
await expect(this.token.connect(this.initialHolder).depositFor(this.token, ethers.MaxUint256))
.to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver')
.withArgs(this.token.target);
});
});
describe('withdraw', function () {
beforeEach(async function () {
await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder });
await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder });
await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply);
await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply);
});
it('missing balance', async function () {
await expectRevertCustomError(
this.token.withdrawTo(initialHolder, MAX_UINT256, { from: initialHolder }),
'ERC20InsufficientBalance',
[initialHolder, initialSupply, MAX_UINT256],
);
it('reverts when inssuficient balance', async function () {
await expect(this.token.connect(this.initialHolder).withdrawTo(this.initialHolder, ethers.MaxInt256))
.to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance')
.withArgs(this.initialHolder.address, initialSupply, ethers.MaxInt256);
});
it('valid', async function () {
const value = new BN(42);
const { tx } = await this.token.withdrawTo(initialHolder, value, { from: initialHolder });
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
from: this.token.address,
to: initialHolder,
value: value,
});
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
from: initialHolder,
to: ZERO_ADDRESS,
value: value,
});
it('executes when operation is valid', async function () {
const value = 42n;
const tx = await this.token.connect(this.initialHolder).withdrawTo(this.initialHolder, value);
await expect(tx)
.to.emit(this.underlying, 'Transfer')
.withArgs(this.token.target, this.initialHolder.address, value)
.to.emit(this.token, 'Transfer')
.withArgs(this.initialHolder.address, ethers.ZeroAddress, value);
await expect(tx).to.changeTokenBalances(this.underlying, [this.token, this.initialHolder], [-value, value]);
await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, -value);
});
it('entire balance', async function () {
const { tx } = await this.token.withdrawTo(initialHolder, initialSupply, { from: initialHolder });
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
from: this.token.address,
to: initialHolder,
value: initialSupply,
});
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
from: initialHolder,
to: ZERO_ADDRESS,
value: initialSupply,
});
const tx = await this.token.connect(this.initialHolder).withdrawTo(this.initialHolder, initialSupply);
await expect(tx)
.to.emit(this.underlying, 'Transfer')
.withArgs(this.token.target, this.initialHolder.address, initialSupply)
.to.emit(this.token, 'Transfer')
.withArgs(this.initialHolder.address, ethers.ZeroAddress, initialSupply);
await expect(tx).to.changeTokenBalances(
this.underlying,
[this.token, this.initialHolder],
[-initialSupply, initialSupply],
);
await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, -initialSupply);
});
it('to other account', async function () {
const { tx } = await this.token.withdrawTo(receiver, initialSupply, { from: initialHolder });
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
from: this.token.address,
to: receiver,
value: initialSupply,
});
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
from: initialHolder,
to: ZERO_ADDRESS,
value: initialSupply,
});
const tx = await this.token.connect(this.initialHolder).withdrawTo(this.recipient, initialSupply);
await expect(tx)
.to.emit(this.underlying, 'Transfer')
.withArgs(this.token.target, this.recipient.address, initialSupply)
.to.emit(this.token, 'Transfer')
.withArgs(this.initialHolder.address, ethers.ZeroAddress, initialSupply);
await expect(tx).to.changeTokenBalances(
this.underlying,
[this.token, this.initialHolder, this.recipient],
[-initialSupply, 0, initialSupply],
);
await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, -initialSupply);
});
it('reverts withdrawing to the wrapper contract', async function () {
expectRevertCustomError(
this.token.withdrawTo(this.token.address, initialSupply, { from: initialHolder }),
'ERC20InvalidReceiver',
[this.token.address],
);
await expect(this.token.connect(this.initialHolder).withdrawTo(this.token, initialSupply))
.to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver')
.withArgs(this.token.target);
});
});
describe('recover', function () {
it('nothing to recover', async function () {
await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder });
await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder });
const { tx } = await this.token.$_recover(receiver);
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
from: ZERO_ADDRESS,
to: receiver,
value: '0',
});
await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply);
await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply);
const tx = await this.token.$_recover(this.recipient);
await expect(tx).to.emit(this.token, 'Transfer').withArgs(ethers.ZeroAddress, this.recipient.address, 0n);
await expect(tx).to.changeTokenBalance(this.token, this.recipient, 0);
});
it('something to recover', async function () {
await this.underlying.transfer(this.token.address, initialSupply, { from: initialHolder });
const { tx } = await this.token.$_recover(receiver);
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
from: ZERO_ADDRESS,
to: receiver,
value: initialSupply,
});
await this.underlying.connect(this.initialHolder).transfer(this.token, initialSupply);
const tx = await this.token.$_recover(this.recipient);
await expect(tx)
.to.emit(this.token, 'Transfer')
.withArgs(ethers.ZeroAddress, this.recipient.address, initialSupply);
await expect(tx).to.changeTokenBalance(this.token, this.recipient, initialSupply);
});
});
describe('erc20 behaviour', function () {
beforeEach(async function () {
await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder });
await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder });
await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply);
await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply);
});
shouldBehaveLikeERC20(initialSupply, accounts);
shouldBehaveLikeERC20(initialSupply);
});
});

Loading…
Cancel
Save