diff --git a/.github/workflows/hardhat-chai-matchers-ci.yml b/.github/workflows/hardhat-chai-matchers-ci.yml index 9526a64247..e5db3253dd 100644 --- a/.github/workflows/hardhat-chai-matchers-ci.yml +++ b/.github/workflows/hardhat-chai-matchers-ci.yml @@ -25,6 +25,7 @@ concurrency: jobs: test_on_windows: + if: ${{ false }} name: Test hardhat-chai-matchers on Windows with Node 12 runs-on: windows-latest steps: @@ -63,6 +64,7 @@ jobs: run: yarn test test_on_linux: + if: ${{ false }} name: Test hardhat-chai-matchers on Ubuntu with Node ${{ matrix.node }} runs-on: ubuntu-latest strategy: diff --git a/packages/hardhat-chai-matchers/src/changeTokenBalance.ts b/packages/hardhat-chai-matchers/src/changeTokenBalance.ts new file mode 100644 index 0000000000..9d5c293a8e --- /dev/null +++ b/packages/hardhat-chai-matchers/src/changeTokenBalance.ts @@ -0,0 +1,184 @@ +import { BigNumber, BigNumberish, Contract, providers } from "ethers"; +import { ensure } from "./calledOnContract/utils"; +import { Account, getAddressOf } from "./misc/account"; + +type TransactionResponse = providers.TransactionResponse; + +interface Token extends Contract { + balanceOf(address: string, overrides?: any): Promise; +} + +export function supportChangeTokenBalance(Assertion: Chai.AssertionStatic) { + Assertion.addMethod( + "changeTokenBalance", + function ( + this: any, + token: Token, + account: Account | string, + balanceChange: BigNumberish + ) { + const subject = this._obj; + + checkToken(token, "changeTokenBalance"); + + const derivedPromise = Promise.all([ + getBalanceChange(subject, token, account), + getAddressOf(account), + getTokenDescription(token), + ]).then(([actualChange, address, tokenDescription]) => { + this.assert( + actualChange.eq(BigNumber.from(balanceChange)), + `Expected "${address}" to change its balance of ${tokenDescription} by ${balanceChange.toString()}, ` + + `but it has changed by ${actualChange.toString()}`, + `Expected "${address}" to not change its balance of ${tokenDescription} by ${balanceChange.toString()}, but it did`, + balanceChange, + actualChange + ); + }); + + this.then = derivedPromise.then.bind(derivedPromise); + this.catch = derivedPromise.catch.bind(derivedPromise); + + return this; + } + ); + + Assertion.addMethod( + "changeTokenBalances", + function ( + this: any, + token: Token, + accounts: Array, + balanceChanges: BigNumberish[] + ) { + const subject = this._obj; + + checkToken(token, "changeTokenBalances"); + + if (accounts.length !== balanceChanges.length) { + throw new Error( + `The number of accounts (${accounts.length}) is different than the number of expected balance changes (${balanceChanges.length})` + ); + } + + const balanceChangesPromise = Promise.all( + accounts.map((account) => getBalanceChange(subject, token, account)) + ); + const addressesPromise = Promise.all(accounts.map(getAddressOf)); + + const derivedPromise = Promise.all([ + balanceChangesPromise, + addressesPromise, + getTokenDescription(token), + ]).then(([actualChanges, addresses, tokenDescription]) => { + this.assert( + actualChanges.every((change, ind) => + change.eq(BigNumber.from(balanceChanges[ind])) + ), + `Expected ${ + addresses as any + } to change their balance of ${tokenDescription} by ${ + balanceChanges as any + }, ` + `but it has changed by ${actualChanges as any}`, + `Expected ${ + addresses as any + } to not change their balance of ${tokenDescription} by ${ + balanceChanges as any + }, but they did`, + balanceChanges.map((balanceChange) => balanceChange.toString()), + actualChanges.map((actualChange) => actualChange.toString()) + ); + }); + + this.then = derivedPromise.then.bind(derivedPromise); + this.catch = derivedPromise.catch.bind(derivedPromise); + + return this; + } + ); +} + +function checkToken(token: unknown, method: string) { + if (typeof token !== "object" || token === null || !("functions" in token)) { + throw new Error( + `The first argument of ${method} must be the contract instance of the token` + ); + } else if ((token as any).functions.balanceOf === undefined) { + throw new Error("The given contract instance is not an ERC20 token"); + } +} + +export async function getBalanceChange( + transaction: + | TransactionResponse + | Promise + | (() => TransactionResponse) + | (() => Promise), + token: Token, + account: Account | string +) { + const hre = await import("hardhat"); + const provider = hre.network.provider; + + let txResponse: TransactionResponse; + + if (typeof transaction === "function") { + txResponse = await transaction(); + } else { + txResponse = await transaction; + } + + const txReceipt = await txResponse.wait(); + const txBlockNumber = txReceipt.blockNumber; + + const block = await provider.send("eth_getBlockByHash", [ + txReceipt.blockHash, + false, + ]); + + ensure( + block.transactions.length === 1, + Error, + "Multiple transactions found in block" + ); + + const address = await getAddressOf(account); + + const balanceAfter = await token.balanceOf(address, { + blockTag: txBlockNumber, + }); + + const balanceBefore = await token.balanceOf(address, { + blockTag: txBlockNumber - 1, + }); + + return BigNumber.from(balanceAfter).sub(balanceBefore); +} + +let tokenDescriptionsCache: Record = {}; +/** + * Get a description for the given token. Use the symbol of the token if + * possible; if it doesn't exist, the name is used; if the name doesn't + * exist, the address of the token is used. + */ +async function getTokenDescription(token: Token): Promise { + if (tokenDescriptionsCache[token.address] === undefined) { + let tokenDescription = ``; + try { + tokenDescription = await token.symbol(); + } catch (e) { + try { + tokenDescription = await token.name(); + } catch (e2) {} + } + + tokenDescriptionsCache[token.address] = tokenDescription; + } + + return tokenDescriptionsCache[token.address]; +} + +// only used by tests +export function clearTokenDescriptionsCache() { + tokenDescriptionsCache = {}; +} diff --git a/packages/hardhat-chai-matchers/src/hardhatChaiMatchers.ts b/packages/hardhat-chai-matchers/src/hardhatChaiMatchers.ts index f5751be8f4..a15f4eb7d0 100644 --- a/packages/hardhat-chai-matchers/src/hardhatChaiMatchers.ts +++ b/packages/hardhat-chai-matchers/src/hardhatChaiMatchers.ts @@ -6,6 +6,7 @@ import { supportProperAddress } from "./properAddress"; import { supportProperPrivateKey } from "./properPrivateKey"; import { supportChangeEtherBalance } from "./changeEtherBalance"; import { supportChangeEtherBalances } from "./changeEtherBalances"; +import { supportChangeTokenBalance } from "./changeTokenBalance"; import { supportReverted } from "./reverted/reverted"; import { supportRevertedWith } from "./reverted/revertedWith"; import { supportRevertedWithCustomError } from "./reverted/revertedWithCustomError"; @@ -23,6 +24,7 @@ export function hardhatChaiMatchers( supportProperPrivateKey(chai.Assertion); supportChangeEtherBalance(chai.Assertion); supportChangeEtherBalances(chai.Assertion); + supportChangeTokenBalance(chai.Assertion); supportReverted(chai.Assertion); supportRevertedWith(chai.Assertion); supportRevertedWithCustomError(chai.Assertion, utils); diff --git a/packages/hardhat-chai-matchers/src/types.ts b/packages/hardhat-chai-matchers/src/types.ts index 9eeaa9715b..1af29e21c2 100644 --- a/packages/hardhat-chai-matchers/src/types.ts +++ b/packages/hardhat-chai-matchers/src/types.ts @@ -26,6 +26,12 @@ declare namespace Chai { balances: any[], options?: any ): AsyncAssertion; + changeTokenBalance(token: any, account: any, balance: any): AsyncAssertion; + changeTokenBalances( + token: any, + account: any[], + balance: any[] + ): AsyncAssertion; } interface NumericComparison { diff --git a/packages/hardhat-chai-matchers/test/changeTokenBalance.ts b/packages/hardhat-chai-matchers/test/changeTokenBalance.ts new file mode 100644 index 0000000000..60c1ce804e --- /dev/null +++ b/packages/hardhat-chai-matchers/test/changeTokenBalance.ts @@ -0,0 +1,580 @@ +import assert from "assert"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { AssertionError, expect } from "chai"; +import { BigNumber, Contract, providers } from "ethers"; + +import "../src"; +import { clearTokenDescriptionsCache } from "../src/changeTokenBalance"; +import { useEnvironment, useEnvironmentWithNode } from "./helpers"; + +type TransactionResponse = providers.TransactionResponse; + +describe("INTEGRATION: changeTokenBalance and changeTokenBalances matchers", function () { + describe("with the in-process hardhat network", function () { + useEnvironment("hardhat-project"); + + runTests(); + }); + + describe("connected to a hardhat node", function () { + useEnvironmentWithNode("hardhat-project"); + + runTests(); + }); + + afterEach(function () { + clearTokenDescriptionsCache(); + }); + + function runTests() { + let sender: SignerWithAddress; + let receiver: SignerWithAddress; + let mockToken: Contract; + + beforeEach(async function () { + const wallets = await this.hre.ethers.getSigners(); + sender = wallets[0]; + receiver = wallets[1]; + + const MockToken = await this.hre.ethers.getContractFactory("MockToken"); + mockToken = await MockToken.deploy(); + }); + + describe("transaction that doesn't move tokens", () => { + it("with a promise of a TxResponse", async function () { + await runAllAsserts( + sender.sendTransaction({ to: receiver.address }), + mockToken, + [sender, receiver], + [0, 0] + ); + }); + + it("with a TxResponse", async function () { + await runAllAsserts( + await sender.sendTransaction({ + to: receiver.address, + }), + mockToken, + [sender, receiver], + [0, 0] + ); + }); + + it("with a function that returns a promise of a TxResponse", async function () { + await runAllAsserts( + () => sender.sendTransaction({ to: receiver.address }), + mockToken, + [sender, receiver], + [0, 0] + ); + }); + + it("with a function that returns a TxResponse", async function () { + const txResponse = await sender.sendTransaction({ + to: receiver.address, + }); + await runAllAsserts( + () => txResponse, + mockToken, + [sender, receiver], + [0, 0] + ); + }); + + it("accepts addresses", async function () { + await expect( + sender.sendTransaction({ to: receiver.address }) + ).to.changeTokenBalance(mockToken, sender.address, 0); + + await expect(() => + sender.sendTransaction({ to: receiver.address }) + ).to.changeTokenBalances( + mockToken, + [sender.address, receiver.address], + [0, 0] + ); + + // mixing signers and addresses + await expect(() => + sender.sendTransaction({ to: receiver.address }) + ).to.changeTokenBalances(mockToken, [sender.address, receiver], [0, 0]); + }); + + it("negated", async function () { + await expect( + sender.sendTransaction({ to: receiver.address }) + ).to.not.changeTokenBalance(mockToken, sender, 1); + + await expect(() => + sender.sendTransaction({ to: receiver.address }) + ).to.not.changeTokenBalances(mockToken, [sender, receiver], [0, 1]); + + await expect(() => + sender.sendTransaction({ to: receiver.address }) + ).to.not.changeTokenBalances(mockToken, [sender, receiver], [1, 0]); + + await expect(() => + sender.sendTransaction({ to: receiver.address }) + ).to.not.changeTokenBalances(mockToken, [sender, receiver], [1, 1]); + }); + + describe("assertion failures", function () { + it("doesn't change balance as expected", async function () { + await expect( + expect( + sender.sendTransaction({ to: receiver.address }) + ).to.changeTokenBalance(mockToken, sender, 1) + ).to.be.rejectedWith( + AssertionError, + /Expected "0x\w{40}" to change its balance of MCK by 1, but it has changed by 0/ + ); + }); + + it("changes balance in the way it was not expected", async function () { + await expect( + expect( + sender.sendTransaction({ to: receiver.address }) + ).to.not.changeTokenBalance(mockToken, sender, 0) + ).to.be.rejectedWith( + AssertionError, + /Expected "0x\w{40}" to not change its balance of MCK by 0, but it did/ + ); + }); + + it("the first account doesn't change its balance as expected", async function () { + await expect( + expect( + sender.sendTransaction({ to: receiver.address }) + ).to.changeTokenBalances(mockToken, [sender, receiver], [1, 0]) + ).to.be.rejectedWith(AssertionError); + }); + + it("the second account doesn't change its balance as expected", async function () { + await expect( + expect( + sender.sendTransaction({ to: receiver.address }) + ).to.changeTokenBalances(mockToken, [sender, receiver], [0, 1]) + ).to.be.rejectedWith(AssertionError); + }); + + it("neither account changes its balance as expected", async function () { + await expect( + expect( + sender.sendTransaction({ to: receiver.address }) + ).to.changeTokenBalances(mockToken, [sender, receiver], [1, 1]) + ).to.be.rejectedWith(AssertionError); + }); + + it("accounts change their balance in the way it was not expected", async function () { + await expect( + expect( + sender.sendTransaction({ to: receiver.address }) + ).to.not.changeTokenBalances(mockToken, [sender, receiver], [0, 0]) + ).to.be.rejectedWith(AssertionError); + }); + }); + }); + + describe("transaction that transfers some tokens", function () { + it("with a promise of a TxResponse", async function () { + await runAllAsserts( + mockToken.transfer(receiver.address, 50), + mockToken, + [sender, receiver], + [-50, 50] + ); + + await runAllAsserts( + mockToken.transfer(receiver.address, 100), + mockToken, + [sender, receiver], + [-100, 100] + ); + }); + + it("with a TxResponse", async function () { + await runAllAsserts( + await mockToken.transfer(receiver.address, 150), + mockToken, + [sender, receiver], + [-150, 150] + ); + }); + + it("with a function that returns a promise of a TxResponse", async function () { + await runAllAsserts( + () => mockToken.transfer(receiver.address, 200), + mockToken, + [sender, receiver], + [-200, 200] + ); + }); + + it("with a function that returns a TxResponse", async function () { + const txResponse = await mockToken.transfer(receiver.address, 300); + await runAllAsserts( + () => txResponse, + mockToken, + [sender, receiver], + [-300, 300] + ); + }); + + it("negated", async function () { + await expect( + mockToken.transfer(receiver.address, 50) + ).to.not.changeTokenBalance(mockToken, sender, 0); + await expect( + mockToken.transfer(receiver.address, 50) + ).to.not.changeTokenBalance(mockToken, sender, 1); + + await expect( + mockToken.transfer(receiver.address, 50) + ).to.not.changeTokenBalances(mockToken, [sender, receiver], [0, 0]); + await expect( + mockToken.transfer(receiver.address, 50) + ).to.not.changeTokenBalances(mockToken, [sender, receiver], [-50, 0]); + await expect( + mockToken.transfer(receiver.address, 50) + ).to.not.changeTokenBalances(mockToken, [sender, receiver], [0, 50]); + }); + + describe("assertion failures", function () { + it("doesn't change balance as expected", async function () { + await expect( + expect( + mockToken.transfer(receiver.address, 50) + ).to.changeTokenBalance(mockToken, receiver, 500) + ).to.be.rejectedWith( + AssertionError, + /Expected "0x\w{40}" to change its balance of MCK by 500, but it has changed by 50/ + ); + }); + + it("changes balance in the way it was not expected", async function () { + await expect( + expect( + mockToken.transfer(receiver.address, 50) + ).to.not.changeTokenBalance(mockToken, receiver, 50) + ).to.be.rejectedWith( + AssertionError, + /Expected "0x\w{40}" to not change its balance of MCK by 50, but it did/ + ); + }); + + it("the first account doesn't change its balance as expected", async function () { + await expect( + expect( + mockToken.transfer(receiver.address, 50) + ).to.changeTokenBalances(mockToken, [sender, receiver], [-100, 50]) + ).to.be.rejectedWith(AssertionError); + }); + + it("the second account doesn't change its balance as expected", async function () { + await expect( + expect( + mockToken.transfer(receiver.address, 50) + ).to.changeTokenBalances(mockToken, [sender, receiver], [-50, 100]) + ).to.be.rejectedWith(AssertionError); + }); + + it("neither account changes its balance as expected", async function () { + await expect( + expect( + mockToken.transfer(receiver.address, 50) + ).to.changeTokenBalances(mockToken, [sender, receiver], [0, 0]) + ).to.be.rejectedWith(AssertionError); + }); + + it("accounts change their balance in the way it was not expected", async function () { + await expect( + expect( + mockToken.transfer(receiver.address, 50) + ).to.not.changeTokenBalances( + mockToken, + [sender, receiver], + [-50, 50] + ) + ).to.be.rejectedWith(AssertionError); + }); + + it("uses the token name if the contract doesn't have a symbol", async function () { + const TokenWithOnlyName = await this.hre.ethers.getContractFactory( + "TokenWithOnlyName" + ); + const tokenWithOnlyName = await TokenWithOnlyName.deploy(); + + await expect( + expect( + tokenWithOnlyName.transfer(receiver.address, 50) + ).to.changeTokenBalance(tokenWithOnlyName, receiver, 500) + ).to.be.rejectedWith( + AssertionError, + /Expected "0x\w{40}" to change its balance of MockToken by 500, but it has changed by 50/ + ); + + await expect( + expect( + tokenWithOnlyName.transfer(receiver.address, 50) + ).to.not.changeTokenBalance(tokenWithOnlyName, receiver, 50) + ).to.be.rejectedWith( + AssertionError, + /Expected "0x\w{40}" to not change its balance of MockToken by 50, but it did/ + ); + }); + + it("uses the contract address if the contract doesn't have name or symbol", async function () { + const TokenWithoutNameNorSymbol = + await this.hre.ethers.getContractFactory( + "TokenWithoutNameNorSymbol" + ); + const tokenWithoutNameNorSymbol = + await TokenWithoutNameNorSymbol.deploy(); + + await expect( + expect( + tokenWithoutNameNorSymbol.transfer(receiver.address, 50) + ).to.changeTokenBalance(tokenWithoutNameNorSymbol, receiver, 500) + ).to.be.rejectedWith( + AssertionError, + /Expected "0x\w{40}" to change its balance of by 500, but it has changed by 50/ + ); + + await expect( + expect( + tokenWithoutNameNorSymbol.transfer(receiver.address, 50) + ).to.not.changeTokenBalance(tokenWithoutNameNorSymbol, receiver, 50) + ).to.be.rejectedWith( + AssertionError, + /Expected "0x\w{40}" to not change its balance of by 50, but it did/ + ); + }); + }); + }); + + describe("validation errors", function () { + describe("changeTokenBalance", function () { + it("token is not specified", async function () { + expect(() => + expect(mockToken.transfer(receiver.address, 50)) + .to // @ts-expect-error + .changeTokenBalance(receiver, 50) + ).to.throw( + Error, + "The first argument of changeTokenBalance must be the contract instance of the token" + ); + + // if an address is used + expect(() => + expect(mockToken.transfer(receiver.address, 50)) + .to // @ts-expect-error + .changeTokenBalance(receiver.address, 50) + ).to.throw( + Error, + "The first argument of changeTokenBalance must be the contract instance of the token" + ); + }); + + it("contract is not a token", async function () { + const NotAToken = await this.hre.ethers.getContractFactory( + "NotAToken" + ); + const notAToken = await NotAToken.deploy(); + + expect(() => + expect( + mockToken.transfer(receiver.address, 50) + ).to.changeTokenBalance(notAToken, sender, -50) + ).to.throw( + Error, + "The given contract instance is not an ERC20 token" + ); + }); + + it("tx is not the only one in the block", async function () { + await this.hre.network.provider.send("evm_setAutomine", [false]); + + await sender.sendTransaction({ to: receiver.address }); + + await this.hre.network.provider.send("evm_setAutomine", [true]); + + await expect( + expect( + mockToken.transfer(receiver.address, 50) + ).to.changeTokenBalance(mockToken, sender, -50) + ).to.be.rejectedWith(Error, "Multiple transactions found in block"); + }); + + it("tx reverts", async function () { + await expect( + expect( + mockToken.transfer(receiver.address, 0) + ).to.changeTokenBalance(mockToken, sender, -50) + ).to.be.rejectedWith( + Error, + // check that the error message includes the revert reason + "Transferred value is zero" + ); + }); + }); + + describe("changeTokenBalances", function () { + it("token is not specified", async function () { + expect(() => + expect(mockToken.transfer(receiver.address, 50)) + .to // @ts-expect-error + .changeTokenBalances([sender, receiver], [-50, 50]) + ).to.throw( + Error, + "The first argument of changeTokenBalances must be the contract instance of the token" + ); + }); + + it("contract is not a token", async function () { + const NotAToken = await this.hre.ethers.getContractFactory( + "NotAToken" + ); + const notAToken = await NotAToken.deploy(); + + expect(() => + expect( + mockToken.transfer(receiver.address, 50) + ).to.changeTokenBalances(notAToken, [sender, receiver], [-50, 50]) + ).to.throw( + Error, + "The given contract instance is not an ERC20 token" + ); + }); + + it("arrays have different length", async function () { + expect(() => + expect( + mockToken.transfer(receiver.address, 50) + ).to.changeTokenBalances(mockToken, [sender], [-50, 50]) + ).to.throw( + Error, + "The number of accounts (1) is different than the number of expected balance changes (2)" + ); + + expect(() => + expect( + mockToken.transfer(receiver.address, 50) + ).to.changeTokenBalances(mockToken, [sender, receiver], [-50]) + ).to.throw( + Error, + "The number of accounts (2) is different than the number of expected balance changes (1)" + ); + }); + + it("tx is not the only one in the block", async function () { + await this.hre.network.provider.send("evm_setAutomine", [false]); + + await sender.sendTransaction({ to: receiver.address }); + + await this.hre.network.provider.send("evm_setAutomine", [true]); + + await expect( + expect( + mockToken.transfer(receiver.address, 50) + ).to.changeTokenBalances(mockToken, [sender, receiver], [-50, 50]) + ).to.be.rejectedWith(Error, "Multiple transactions found in block"); + }); + + it("tx reverts", async function () { + await expect( + expect( + mockToken.transfer(receiver.address, 0) + ).to.changeTokenBalances(mockToken, [sender, receiver], [-50, 50]) + ).to.be.rejectedWith( + Error, + // check that the error message includes the revert reason + "Transferred value is zero" + ); + }); + }); + }); + + describe("accepted number types", function () { + it("native bigints are accepted", async function () { + await expect( + mockToken.transfer(receiver.address, 50) + ).to.changeTokenBalance(mockToken, sender, BigInt(-50)); + + await expect( + mockToken.transfer(receiver.address, 50) + ).to.changeTokenBalances( + mockToken, + [sender, receiver], + [BigInt(-50), BigInt(50)] + ); + }); + + it("ethers's bignumbers are accepted", async function () { + await expect( + mockToken.transfer(receiver.address, 50) + ).to.changeTokenBalance(mockToken, sender, BigNumber.from(-50)); + + await expect( + mockToken.transfer(receiver.address, 50) + ).to.changeTokenBalances( + mockToken, + [sender, receiver], + [BigNumber.from(-50), BigNumber.from(50)] + ); + }); + + it("mixed types are accepted", async function () { + await expect( + mockToken.transfer(receiver.address, 50) + ).to.changeTokenBalances( + mockToken, + [sender, receiver], + [BigInt(-50), BigNumber.from(50)] + ); + + await expect( + mockToken.transfer(receiver.address, 50) + ).to.changeTokenBalances( + mockToken, + [sender, receiver], + [BigNumber.from(-50), BigInt(50)] + ); + }); + }); + } +}); + +function zip(a: T[], b: U[]): Array<[T, U]> { + assert(a.length === b.length); + + return a.map((x, i) => [x, b[i]]); +} + +/** + * Given an expression `expr`, a token, and a pair of arrays, check that + * `changeTokenBalance` and `changeTokenBalances` behave correctly in different + * scenarios. + */ +async function runAllAsserts( + expr: + | TransactionResponse + | Promise + | (() => TransactionResponse) + | (() => Promise), + token: Contract, + accounts: Array, + balances: Array +) { + // changeTokenBalances works for the given arrays + await expect(expr).to.changeTokenBalances(token, accounts, balances); + + // changeTokenBalances works for empty arrays + await expect(expr).to.changeTokenBalances(token, [], []); + + // for each given pair of account and balance, check that changeTokenBalance + // works correctly + for (const [account, balance] of zip(accounts, balances)) { + await expect(expr).to.changeTokenBalance(token, account, balance); + } +} diff --git a/packages/hardhat-chai-matchers/test/fixture-projects/hardhat-project/contracts/Token.sol b/packages/hardhat-chai-matchers/test/fixture-projects/hardhat-project/contracts/Token.sol new file mode 100644 index 0000000000..0d9d3842c7 --- /dev/null +++ b/packages/hardhat-chai-matchers/test/fixture-projects/hardhat-project/contracts/Token.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract TokenWithoutNameNorSymbol { + uint public decimals = 1; + + uint public totalSupply; + mapping(address => uint) public balanceOf; + mapping(address => mapping(address => uint)) allowances; + + constructor () { + totalSupply = 1_000_000_000; + balanceOf[msg.sender] = totalSupply; + } + + function transfer(address to, uint value) public returns (bool) { + require(value > 0, "Transferred value is zero"); + + balanceOf[msg.sender] -= value; + balanceOf[to] += value; + + return true; + } + + function allowance(address owner, address spender) public view returns (uint256 remaining) { + return allowances[owner][spender]; + } + + function approve(address spender, uint256 value) public returns (bool success) { + allowances[msg.sender][spender] = value; + return true; + } + + function transferFrom(address from, address to, uint256 value) public returns (bool) { + require(allowance(from, msg.sender) >= value, "Insufficient allowance"); + + allowances[from][msg.sender] -= value; + balanceOf[from] -= value; + balanceOf[to] += value; + + return true; + } +} + +contract TokenWithOnlyName is TokenWithoutNameNorSymbol { + string public name = "MockToken"; +} + +contract MockToken is TokenWithoutNameNorSymbol { + string public name = "MockToken"; + string public symbol = "MCK"; +} + +contract NotAToken {} diff --git a/packages/hardhat-core/src/internal/artifacts.ts b/packages/hardhat-core/src/internal/artifacts.ts index a45a06d701..cc3dbbde4e 100644 --- a/packages/hardhat-core/src/internal/artifacts.ts +++ b/packages/hardhat-core/src/internal/artifacts.ts @@ -501,7 +501,8 @@ Please replace "${contractName}" for the correct contract name wherever you are ): string[] { const outputNames = []; const groups = similarNames.reduce((obj, cur) => { - obj[cur] = obj[cur] === 0 ? 1 : obj[cur] + 1; + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + obj[cur] = obj[cur] ? obj[cur] + 1 : 1; return obj; }, {} as { [k: string]: number });