7

I want to be able to check using JavaScript/TypeScript & Ethers.js if a contract has a certain function.

I want to do this so that if the contract's function doesn't exist, I want to call another.

I want to do something along the lines of this:

if (contract.functionA) {
  // Other code logic before call
  contract.functionA()
} else {
  // Other code logic before call
  contract.functionB();
}
2
  • If you have the ABI of the contract you can check it that way by parsing the ABI itself. You can check more about ABI format on ethers documentation page. docs.ethers.io/v5/api/utils/abi/formats
    – dHour
    Commented Apr 28, 2022 at 7:38
  • 1
    And if you don't have the ABI, then maybe think again whether you really want to do what you are planning to do. Not knowing the ABI means you don't know the contract you are interacting with, which in turn means you can't trust it. I'm sure there are some edge cases where you really want to run unverified EVM code, but I suspect they are pretty rare.
    – gTcV
    Commented Apr 28, 2022 at 7:42

5 Answers 5

4
+50

While other answers are correct, I think they can be improved as they only check for inclusion of a specific function selector in the bytecode which is not the same as checking if the jump table allows execution given that specific function selector.

For example, the following contract would trigger a false positive if you were to check for the function selector of functionA() : 9febadf2 :

pragma solidity ^0.8.4;

contract FalsePositive {

    function randomFunction() public view {
      bytes4 data = 0x9febadf2;
    }
}

To check for execution, and therefore a relevant entry in the jump table, it might be better to rely on the free gas estimation functions which technically includes the check of other answers and more since it will trigger a (free) execution:

EDIT: Modified the code to account for the other answers (checking the inclusion of the function selector in the bytecode) and added "undecidability" check if a fallback function is defined.

const { ethers } = require("ethers");

const ADDRESS = "THE-CONTRACT-ADDRESS";
const ENDPOINT = "THE-ENDPOINT-ADDRESS";
const ABI = ["function functionA()"];

const provider = new ethers.providers.getDefaultProvider(ENDPOINT);

async function main() {
  const bytecode = await provider.getCode(ADDRESS);

  // No code : "0x" then functionA is definitely not there
  if (bytecode.length <= 2) {
    console.log("No functionA() : no code");
    return;
  }

  // If the bytecode doesn't include the function selector functionA()
  // is definitely not present
  if (!bytecode.includes(ethers.utils.id("functionA()").slice(2, 10))) {
    console.log("No functionA() : no function selector in bytecode");
    return;
  }

  const contract = new ethers.Contract(ADDRESS, ABI, provider);

  // Check if a fallback function is defined : if it is, we cannot answer
  try {
    await provider.estimateGas({ to: ADDRESS });
    console.log(
      "Fallback is present : unable to decide if functionA() is present or not"
    );
    return;
  } catch {}

  // If gas estimation doesn't revert then an execution is possible
  // given the provided function selector
  try {
    await contract.estimateGas.functionA();
    console.log("functionA() potentially included");
  } catch {
    // Otherwise (revert) we assume that there is no entry in the jump table
    // meaning that the contract doesn't include functionA()
    console.log("No functionA() : reverted");
  }
}

main();

Which doesn't trigger a false positive on the previous example. All the previously presented version do have a flaw that cannot be avoided anyway. If you were unlucky, those approaches would be fooled by a function selector collision.

Take the following function : functionA12735121() which happen to also have a function selector of 0x9febadf2, all the approaches mentioned would be fooled and return a false positive. The one that I am presenting, however, would catch a revert in the event of a function selector collision with different parameters, since the mismatching call data length would trigger a revert inside the smart contract.

There is also a possibility of a false negative, in case functionA() does exist, but reverts for some reasons... It could be improved by verifying the revert reason though.

1
  • Using estimateGas is a nice idea. But the solution has the limitation that searching in the bytecode won't work for proxied contracts.
    – Ismael
    Commented May 3, 2022 at 5:09
2

If you do not have an ABI you can do the following flaky check

Note that contracts can always fake their interfaces or methods they support, so this check is only guidelining.

1

First thing, you'd want to fetch the smart contract's deployed bytecode as follows:

const provider = new ethers.providers.InfuraProvider(NETWORK, INFURA_API_KEY);
const bytecode = await provider.getCode(CONTRACT_ADDRESS);

Next, you'll need to compute the method selector of the function signature you're looking for and check if it exists as follows:

// In the following line, you could either provide a complete smart contract
// ABI that is imported from a JSON file; or you could just define it for
// the single function signature that you're searching for.
const abi = ["function methodName(methodType1,methodType2,...)"];

const iface = new ethers.utils.Interface(abi);
const method_selector = iface.getSighash("methodName(methodType1,methodType2,...)").substring(2);

const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
const contract = new ethers.Contract(CONTRACT_ADDRESS, abi, signer);

if(bytecode.includes(method_selector)) {
   /// logic when contract contains the function:
   /// call the method with provided values
   await contract.methodName(value1, value2, ...);
} else {
   /// logic when contract doesn't contain the function
   ...
}
0

I have written some simple scripts that may be helpful for working with Ethereum-based smart contracts and have made them into an npm module.

npm i @sigridjin/contract-validator

It has the following features:

  1. Check if the given contract address exists on the network
  2. Check if the given method signature can be called on the contract
  3. Verify that the given ABI belongs to the contract

None of these are supported by web3.js or ethers.js...

https://github.com/sigridjineth/contract-validation

0

If you have the abi you can create an interface which is a object that contains all the contract functions:

const iface = new ethers.utils.Interface(contract.abi)

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.