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,
},
};
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)