Search code examples
soliditycryptocurrencyhardhat

Hardhat test issue. Solidity contract mistake?


I am new to crypto and just exploring Solidity language. I try to make a simple Solidify token contract with some basic functionality. It should transfer the token and update the balance. However when I run the test that supposed to try add to balance functionality, I get this error:

npx hardhat test   
No need to generate any newer typings.


  MyERC20Contract
    when I transfer 10 tokens
      1) sould transfer tokens correctly


  0 passing (728ms)
  1 failing

  1) MyERC20Contract
       when I transfer 10 tokens
         sould transfer tokens correctly:
     Error: VM Exception while processing transaction: reverted with reason string 'ERC20: transfer amount exceeds balance'
    at ERC20._transfer (contracts/ERC20.sol:49)
    at ERC20.transfer (contracts/ERC20.sol:25)
    at async HardhatNode._mineBlockWithPendingTxs (node_modules/hardhat/src/internal/hardhat-network/provider/node.ts:1773:23)
    at async HardhatNode.mineBlock (node_modules/hardhat/src/internal/hardhat-network/provider/node.ts:466:16)
    at async EthModule._sendTransactionAndReturnHash (node_modules/hardhat/src/internal/hardhat-network/provider/modules/eth.ts:1504:18)
    at async HardhatNetworkProvider.request (node_modules/hardhat/src/internal/hardhat-network/provider/provider.ts:118:18)
    at async EthersProviderWrapper.send (node_modules/@nomiclabs/hardhat-ethers/src/internal/ethers-provider-wrapper.ts:13:20)

Do I make some mistake I'm not aware of?

My test file:

import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { expect } from "chai";
import { ethers } from "hardhat";
import { ERC20 } from "../typechain";

describe("MyERC20Contract", function() {

  let MyERC20Contract: ERC20;
  let someAddress: SignerWithAddress;
  let someOtherAddress: SignerWithAddress;

      beforeEach(async function() {
        const ERC20ContractFactory = await ethers.getContractFactory("ERC20");
        MyERC20Contract = await ERC20ContractFactory.deploy("Hello","SYM");
        await MyERC20Contract.deployed();
        someAddress = (await ethers.getSigners())[1];
        someOtherAddress = (await ethers.getSigners())[2];
      });

      describe("When I have 10 tokens", function() {
        beforeEach(async function() {
            await MyERC20Contract.transfer(someAddress.address, 10);
        });
      });

      describe("when I transfer 10 tokens",  function() {
        it("sould transfer tokens correctly", async function() {
          await MyERC20Contract
          .connect(someAddress)
          .transfer(someOtherAddress.address, 10);
          
          expect(
            await MyERC20Contract.balanceOf(someOtherAddress.address)
            ).to.equal(10);
        });
        
      });
    
});

Mys .sol contract:

//SPDX-License-Identifier: Unlicense: MIT
pragma solidity ^0.8.0;

contract ERC20 {
    uint256 public totalSupply;
    string public name;
    string public symbol;
   

    mapping (address => uint256) public balanceOf;
    mapping (address => mapping (address => uint256)) public allowance;


    constructor(string memory name_, string memory symbol_) {
        name = name_;
        symbol = symbol_;
        _mint(msg.sender, 100e18);
    } 

    function decimals() external pure returns (uint8) {
        return 18;
    }

    function transfer(address recipient, uint256 amount) external returns (bool) {
            return _transfer(msg.sender, recipient, amount);
            
    }

    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) {

        uint256 currentAllowance = allowance[sender][msg.sender];

        require(currentAllowance >= amount, "ERC20: Transfer amount exceeds allowance" ) ;

        allowance[sender][msg.sender] = currentAllowance - amount;

        return _transfer(sender, recipient, amount);
            
    }

    function approve(address spender, uint256 amount) external returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }

    function _transfer(address sender, address recipient, uint256 amount) private returns (bool) {
            require(recipient != address(0), "ERC20: transfer to zero address");
            uint256 senderBalance = balanceOf[sender];
            require(senderBalance >= amount, "ERC20: transfer amount exceeds balance");
            balanceOf[sender] = senderBalance - amount;
            balanceOf[recipient] += amount;

            return true;
            
    }

    function _mint(address to, uint256 amount) internal {
        require(recipient != address(0), "ERC20: transfer to zero address");
        totalSupply += amount;
        balanceOf[to] +=amount;
    }
 
}

Solution

  • MyERC20Contract = await ERC20ContractFactory.deploy("Hello","SYM");
    

    Since your snippet doesn't specify from which address is the deploying tranaction, the contract is deployed from the first address (index 0).

    The 0th address receives the tokens from the constructor, other addresses don't have any tokens.

    constructor(string memory name_, string memory symbol_) {
        name = name_;
        symbol = symbol_;
        _mint(msg.sender, 100e18);
    }
    

    But then your snippet tries to send tokens from the 2nd address (index 1).

    someAddress = (await ethers.getSigners())[1];
    
    it("sould transfer tokens correctly", async function() {
        await MyERC20Contract
        .connect(someAddress)
        .transfer(someOtherAddress.address, 10);
    

    Because the someAddress does not own any tokens, the transaction fails.

    Solution: Either fund the someAddress in your Solidity code as well, or send the tokens from the deployer address (currently the only address that has non-zero token balance).


    Edit:

    There is a beforeEach() in your When I have 10 tokens block, but that's applied only to this specific block - not to the when I transfer 10 tokens block that performs the failed transfer.

    So another solution is to move this specific beforeEach() to the when I transfer block but, based on the context, it doesn't seem like a very clean approach. A good practice is to have as few as possible test cases not affecting each other.