Aleks Shenshin
Aleks Shenshin

Reputation: 2196

How to test the Solidity fallback() function via Hardhat?

I have a Solidity smart contract Demo which I am developing in Hardhat and testing on the RSK Testnet.

//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

contract Demo {
    event Error(string);
    fallback() external {
      emit Error("call of a non-existent function");
    }
}

I want to make sure that the fallback function is called and the event Error is emitted. To this end, I am trying to call a nonExistentFunction on the smart contract:

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

describe('Demo', () => {
  let deployer;
  let demoContract;
    
  before(async () => {
    [deployer] = await ethers.getSigners();
    const factory = await ethers.getContractFactory('Demo');
    demoContract = await factory.deploy().then((res) => res.deployed());
  });
    
  it('should invoke the fallback function', async () => {
    const tx = demoContract.nonExistentFunction();
    await expect(tx)
      .to.emit(demoContract, 'Error')
      .withArgs('call of a non-existent function');
  });
});

However Hardhat throws a TypeError even before it actually connects to the smart contract on RSK:

  Demo
    1) should invoke the fallback function


  0 passing (555ms)
  1 failing

  1) Demo
       should invoke the fallback function:
     TypeError: demoContract.nonExistentFunction is not a function
      at Context.<anonymous> (test/Demo.js:13:29)
      at processImmediate (internal/timers.js:461:21)

How can I outsmart Hardhat/Ethers.js and finally be able to call non-existent function thus invoking the fallback function in the smart contract?

For reference, this is my hardhat.config.js

require('@nomiclabs/hardhat-waffle');
const { mnemonic } = require('./.secret.json');

module.exports = {
  solidity: '0.8.4',
  networks: {
    hardhat: {},
    rsktestnet: {
      chainId: 31,
      url: 'https://public-node.testnet.rsk.co/',
      accounts: {
        mnemonic,
        path: "m/44'/60'/0'/0",
      },
    },
  },
  mocha: {
    timeout: 600000,
  },
};

Upvotes: 5

Views: 1743

Answers (2)

bguiz
bguiz

Reputation: 28617

You can use the approach of injecting a non-existent function signature into the smart contract object (ethers.Contract). Create a function signature for nonExistentFunction:

const nonExistentFuncSignature =
  'nonExistentFunction(uint256,uint256)';

Note that the parameter list should contain no whitespace, and consists of parameter types only (no parameter names).

Then instantiate a new smart contract object.

When you do this you need to modify the ABI for Demo such that it includes this additional function signature.:

const fakeDemoContract = new ethers.Contract(
  demoContract.address,
  [
    ...demoContract.interface.fragments,
    `function ${nonExistentFuncSignature}`,
  ],
  deployer,
);

Note that the contract that is deployed is the same as before - it does not contain this new function. However the client interacting with this smart contract - the tests in this case - does think that the smart contract has this function now.

At this point, you'll be able to run your original test, with a minor modification:

const tx = fakeDemoContract[nonExistentFuncSignature](8, 9);
await expect(tx)
  .to.emit(demoContract, 'Error')
  .withArgs('call of a non-existent function');

Full test:

it('should invoke the fallback function', async () => {
    const nonExistentFuncSignature = 'nonExistentFunc(uint256,uint256)';
    const fakeDemoContract = new ethers.Contract(
      demoContract.address,
      [
        ...demoContract.interface.fragments,
        `function ${nonExistentFuncSignature}`,
      ],
      deployer,
    );
    const tx = fakeDemoContract[nonExistentFuncSignature](8, 9);
    await expect(tx)
      .to.emit(demoContract, 'Error')
      .withArgs('call of a non-existent function');
 });

Test result:

  Demo
    ✔ should invoke the fallback function (77933ms)


  1 passing (2m)

Upvotes: 4

Petr Hejda
Petr Hejda

Reputation: 43561

A transaction executing a function contains the function selector following its (ABI-encoded) input params in the data field.

The fallback() function gets executed when the transaction data field starts with a selector that does not match any existing function. For example an empty selector.

So you can generate a transaction to the contract address, with empty data field, which invokes the fallback() function.

it('should invoke the fallback function', async () => {
    const tx = deployer.sendTransaction({
        to: demoContract.address,
        data: "0x",
    });
    await expect(tx)
        .to.emit(demoContract, 'Error')
        .withArgs('call of a non-existent function');
});

Note: If you also declared the receive() function, it takes precedence over fallback() in case of empty data field. However, fallback() still gets executed for every non-empty mismatching selector, while receive() is only invoked when the selector is empty.

Upvotes: 4

Related Questions