Kaynağa Gözat

Migrate ERC20 and ERC20Wrapper tests to ethersjs (#4743)

Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
Renan Souza 1 yıl önce
ebeveyn
işleme
c35057978f

+ 131 - 195
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;
 
-            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);
   });
 }
 

+ 100 - 95
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);
         });
       });
     });

+ 132 - 135
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);
+
+      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);
   });
 });