Solidity – Hardhat Testing for msg.sender.call: A Comprehensive Guide

ethers.jshardhatsolidity

I use hardhat to test this function:

// Pool.sol

function withdraw(uint256 _withdrawAmount) public payable returns (bool) {
    require(_withdrawAmount <= getBalance(), "overdrawn");
    balances[msg.sender] -= _withdrawAmount;
    totalContractBalance -= _withdrawAmount;

    (bool sent, ) = msg.sender.call{value: _withdrawAmount}("");
    require(sent, "Failed to withdraw");

    return true;
}

by using:

// test/Pool.js

it('Should withdrawSomeBalance', async function () {
  await pool.connect(addr1).addBalance(5)
  await pool.connect(addr1).withdraw(2)
  expect(await pool.connect(addr1).getBalance()).to.equal(3)
})

I got:

Error: VM Exception while processing transaction: reverted with reason string 'Failed to withdraw'

But when I comment out require(sent, "Failed to withdraw") in Pool.sol it pass the test.

Both addBalance() and getBalance() pass test.
Is there anything I made incorrectly for the contract or the test?
How can I include the require(sent, "Failed to withdraw") and make the whole test more sense? Thanks.

===

Pool.sol

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.4;
import "hardhat/console.sol";

contract Pool {
    uint256 totalContractBalance = 0;
    mapping(address => uint256) balances; // user ETH in wei
function getContractBalance() public view returns (uint256) {
    return totalContractBalance;
}

function addBalance(uint256 _amount) public payable returns (bool) {
    balances[msg.sender] = _amount;
    totalContractBalance += _amount;
    return true;
}

function getBalance() public view returns (uint256) {
    return balances[msg.sender];
}

function withdraw(uint256 _withdrawAmount) public payable returns (bool) {
    require(_withdrawAmount <= getBalance(), "overdrawn");
    balances[msg.sender] -= _withdrawAmount;
    totalContractBalance -= _withdrawAmount;

    (bool sent, ) = msg.sender.call{value: _withdrawAmount}("");
    require(sent, "Failed to withdraw"); // comment out this will pass the test!

    return true;
}

receive() external payable {}
}

/test/Pool.js

const { expect } = require('chai')
const { ethers } = require('hardhat')

describe('Pool', function () {
  let Pool, pool, owner, addr1, addr2

  beforeEach(async () => {
    Pool = await ethers.getContractFactory('Pool')
    pool = await Pool.deploy()
    ;[owner, addr1, addr2, _] = await ethers.getSigners()
    //await pool.deployed();
  })

  describe('Deployment', function () {
    it('Should set the right inital balance', async function () {
      expect(await pool.getContractBalance()).to.equal(0)
    })
  })

  describe('Transactions', function () {
    it('Should return contract balance from addBalance', async function () {
      await pool.addBalance(1)
      //await addSomeBalance.wait()
      expect(await pool.getContractBalance()).to.equal(1)
    })
it('Should return user balance from addBalance', async function () {
  await pool.addBalance(11)
  //await addSomeBalance.wait()
  expect(await pool.getBalance()).to.equal(11)
})

it('Should withdrawSomeBalance', async function () {
  await pool.connect(addr1).addBalance(5)
  //await addSomeBalance.wait()
  await pool.connect(addr1).withdraw(2)
  //await withdrawSomeBalance.wait()
  expect(await pool.connect(addr1).getBalance()).to.equal(3)
})
  })
})

testing output:

  Pool
Deployment
  ✓ Should set the right inital balance
Transactions
  ✓ Should return contract balance from addBalance
  ✓ Should return user balance from addBalance
  1) Should withdrawSomeBalance


  3 passing (1s)
  1 failing

  1) Pool
       Transactions
         Should withdrawSomeBalance:
     Error: VM Exception while processing transaction: reverted with reason string 'Failed to withdraw'

Best Answer

The function withdraw fails because this call returns false indicating the transfer failed

msg.sender.call{value: _withdrawAmount}("");

It fails because the contract doesn't have enough ether.

The script isn't sending ether with addBalance(5) it just increases the internal balances mapping.

To send ether with the call have to write something like this:

addBalance(5, {value: ethers.utils.parseUnits("5", "wei")}).
Related Topic