From c35057978f9d13fee96c0ee34d251c00286d3e47 Mon Sep 17 00:00:00 2001 From: Renan Souza Date: Wed, 29 Nov 2023 19:57:16 +0000 Subject: [PATCH] Migrate ERC20 and ERC20Wrapper tests to ethersjs (#4743) Co-authored-by: Hadrien Croubois --- test/token/ERC20/ERC20.behavior.js | 326 +++++++----------- test/token/ERC20/ERC20.test.js | 195 ++++++----- .../ERC20/extensions/ERC20Wrapper.test.js | 263 +++++++------- 3 files changed, 361 insertions(+), 423 deletions(-) diff --git a/test/token/ERC20/ERC20.behavior.js b/test/token/ERC20/ERC20.behavior.js index b6f8617b2..522df3fcf 100644 --- a/test/token/ERC20/ERC20.behavior.js +++ b/test/token/ERC20/ERC20.behavior.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; + beforeEach(async function () { + this.tx = await this.token + .connect(this.recipient) + .transferFrom(this.initialHolder, this.anotherAccount, value); + }); + 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'); - - expect(await this.token.balanceOf(to)).to.be.bignumber.equal(value); + 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; + beforeEach(async function () { + this.tx = await this.transfer(this.initialHolder, this.recipient, value); + }); + 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'); - - expect(await this.token.balanceOf(to)).to.be.bignumber.equal(value); + 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'); + const value = 0n; + + beforeEach(async function () { + this.tx = await this.transfer(this.initialHolder, this.recipient, value); + }); it('transfers the requested value', async function () { - await transfer.call(this, from, to, value); - - expect(await this.token.balanceOf(from)).to.be.bignumber.equal(balance); - - expect(await this.token.balanceOf(to)).to.be.bignumber.equal('0'); + 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 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 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); }); } diff --git a/test/token/ERC20/ERC20.test.js b/test/token/ERC20/ERC20.test.js index 2191fd8cb..97037a697 100644 --- a/test/token/ERC20/ERC20.test.js +++ b/test/token/ERC20/ERC20.test.js @@ -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); }); }); }); diff --git a/test/token/ERC20/extensions/ERC20Wrapper.test.js b/test/token/ERC20/extensions/ERC20Wrapper.test.js index c54a9e007..b61573edd 100644 --- a/test/token/ERC20/extensions/ERC20Wrapper.test.js +++ b/test/token/ERC20/extensions/ERC20Wrapper.test.js @@ -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); - it('missing approval', async function () { - await expectRevertCustomError( - this.token.depositFor(initialHolder, initialSupply, { from: initialHolder }), - 'ERC20InsufficientAllowance', - [this.token.address, 0, 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 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 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('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('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], ); - }); - - 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, - }); + 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); + it('executes when operation is valid', async function () { + const value = 42n; - 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, - }); + 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 }); + 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(receiver); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: ZERO_ADDRESS, - to: receiver, - value: '0', - }); + 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 }); + await this.underlying.connect(this.initialHolder).transfer(this.token, initialSupply); - const { tx } = await this.token.$_recover(receiver); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: ZERO_ADDRESS, - to: receiver, - value: 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); }); });