6

Using hardhat and chai, is there a way to simulate/force a transaction failure so the error code will execute?

e.g. with the following, I want it to throw the error because the transaction didn't succeed.

function withdraw() public onlyOwner {
    uint amount = address(this).balance;
 
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success, "Failed to withdraw contract balance");
}

Can I do that with chai or another way?

To clarify, the point is to create a test that will execute this line of code

require(success, "Failed to withdraw contract balance");

with a status of success == false (the transaction failed bit) so the unit tests cover that branch of code, and without altering the code of the contract being tested.

I don't want to do something like put in !success or otherwise change the code of the contract being tested. I think that would be bad practice so would like to find another way.

5 Answers 5

5

minimal code to implement Lauri's suggestion

Target.sol (contract being tested)

pragma solidity ^0.8.10;

contract Target {

    address payable owner;

    constructor () {
        owner = payable(msg.sender);
    }

    modifier onlyOwner {
        require(msg.sender == owner, "only owner");
        _;
    }

    function withdraw() external onlyOwner{
        uint _balance = address(this).balance;
        (bool success,) = owner.call{value: _balance}("");
        require(success, "Withdraw transaction failed");
    }
}

TestHelper.sol can't receive value as it has neither receive nor fallback functions

pragma solidity ^0.8.10;

import "./Target.sol";

contract TestHelper {

    Target targetContract;

    constructor () {
        targetContract = new Target();
    }

    function targetWithdraw() external {
        targetContract.withdraw();
    }
}

target.test.js

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("Test target contract withdraw", function () {
    it("reverts as expected on failed transaction", async function () {
        const testHelperFactory = await ethers.getContractFactory('TestHelper')
        testHelper = await testHelperFactory.deploy()
        await testHelper.deployed()
        await expect(testHelper.targetWithdraw()).to.be.revertedWith("Withdraw transaction failed")
    })
});
1

I see at least two approaches:

  1. Test it by using a custom smart contract as the owner and msg.sender . That contract can then refuse the transfer when you want it to.

  2. Change the function to take in the receiver address as a parameter. Then you can easily use a custom contract as a receiver and you can set the contract to refuse the transfer.

I don't think something like chai can help you here, since that's basically just syntactic sugar.

1
  • thanks Lauri. @lauri-peltonen Option 2 is perfect as it doesn't involve altering the code being tested I'll post an answer with the minimal code to implement that approach
    – sola24
    Commented Jun 4, 2022 at 18:35
0

Yes, you can create a contract that will have balance at your tested contract, it should have a receive function that forces revert, this will cause success to be false.

interface IMyContract {
    function withdraw() external;
}

contract RevertTest {

    function callWithdraw(IMyContract cont) public {
        cont.withdraw();
    }


    receive() external payable {
        revert("no thanks");
    }
}

when you call RevertTest.callWithdraw(mycontract.address) it should revert with reason "Failed to withdraw contract balance".

0

Instead of sending ether to msg.sender, send them to a mock contract implementing a receive() function that simply reverts. I.e. replace msg.sender with the address of a contract. like shown in the above answer except that you don't need a helper contract in order to deploy the base contract and call withdraw as the msg.sender.

-1

you can either use truffle-assertions with

npm install truffle-assertions

in your test code

const truffleAssert = require('truffle-assertions');
await truffleAssert.reverts(contractInstance.withdraw());

or you can use a simple try catch with

try {
    await contractInstance.withdraw();
} catch(err) {
    Error = err
}
assert.equal(Error.reason, 'not valid!');
5
  • unless I'm missing something, this won't create/cause a failed transaction which is what I need to have the code branch to revert
    – sola24
    Commented Jun 1, 2022 at 19:51
  • It works, test it. truffle-assertions.revert() expects a smart contract function which fails, and in your tests it will be marked as a successful test if the call to contractInstance.withdraw() fails With the try catch it will work too as in hardhat test a failed contract function returns an exception kalis.me/assert-reverts-solidity-smart-contract-test-truffle Commented Jun 2, 2022 at 14:59
  • yeah, but that's not what the question asks for though. It's very easy to test/catch the result of a failed transaction. What I need is a mechanism to make/create a transaction failure. Without that, the code will never go down the failed branch. I don't need a way to test if something failed, I need a way to make it fail and without hacking/changing the contract code. I want to invoke a failed transaction from outside the contract, just like the real world scenario.
    – sola24
    Commented Jun 2, 2022 at 17:08
  • You want your transaction to fail? That's even easier! Just call it from an account other than the owner If what you are looking for is (bool success, ) = msg.sender.call{value: amount}(""); to fail, have some invalid call in your fallback function of that smart contract which is what msg.sender.call is calling Commented Jun 2, 2022 at 19:06
  • An example would be, create a mock contract just for testing, inherit that contract, and create a fallback function which transfers more eth than the eth it has Commented Jun 2, 2022 at 19:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.