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.