[Ethereum] How to check for the existence of a function in Solidity when I have an address that might implement that function

erc-20solidity

If I'm writing an Ethereum smart contract in Solidity and I know I have an ERC20 address variable (let's call it some_erc20_token), but I don't know whether this address implements the interface of an ERC20 extension EIP (for instance ERC1404 with its detectTransferRestriction function) how do I check if the address implements that function so I can call it?

If this were Java I would use the instanceof operator to check for this, something like

void wagTail(Animal animal) {
  if (animal instanceof Dog) {
    animal = (Dog)animal;
    animal.wagDogTail();
  } else if (animal instance of Cat) {
    animal = (Cat)animal;
    animal.wagCatTail();
  }
}
  

In my solidity contract I want to do something of the form

if (some_erc20_token.detectTransferRestriction != 0) { // this function exists!
  some_erc20_token.detectTransferRestriction(to, from , amount);
}

However that doesn't compile and I get the compiler error: TypeError: Member "detectTransferRestriction" not found or not visible after argument-dependent lookup in contract MyContract.

Best Answer

You can implement a helper function in your contract, which will try to call detectTransferRestriction and return success or failure:

  • Success will indicate that the function exists AND its execution completed successfully
  • Failure will indicate that the function does not exist OR its execution did not complete successfully

There you go:

pragma solidity 0.4.25;

contract MyContract {
    bytes4 private constant FUNC_SELECTOR = bytes4(keccak256("detectTransferRestriction(address,address,uint256)"));

    function callDetectTransferRestriction(address _token, address _to, address _from, uint256 _amount) public returns (bool) {
        bool success;
        bytes memory data = abi.encodeWithSelector(FUNC_SELECTOR, _to, _from, _amount);

        assembly {
            success := call(
                gas,            // gas remaining
                _token,         // destination address
                0,              // no ether
                add(data, 32),  // input buffer (starts after the first 32 bytes in the `data` array)
                mload(data),    // input length (loaded from the first 32 bytes in the `data` array)
                0,              // output buffer
                0               // output length
            )
        }

        return success;
    }
}

I declared the helper function public in order to test it, but you don't have to do that of course.

Here is a Truffle 4.x test:

contract("MyContract", accounts => {
    const TO = "0x".padEnd(42, "1");
    const FROM = "0x".padEnd(42, "2");
    const AMOUNT = "0x".padEnd(66, "3");

    it("test", async () => {
        const myContract = await artifacts.require("MyContract").new();
        const goodToken = await artifacts.require("GoodToken").new();
        const badToken = await artifacts.require("BadToken").new();
        await myContract.callDetectTransferRestriction(goodToken.address, TO, FROM, AMOUNT);
        await myContract.callDetectTransferRestriction(badToken.address, TO, FROM, AMOUNT);
        const goodToken_dummy1 = await goodToken.dummy1();
        const goodToken_dummy2 = await goodToken.dummy2();
        const goodToken_dummy3 = await goodToken.dummy3();
        const badToken_dummy1 = await badToken.dummy1();
        const badToken_dummy2 = await badToken.dummy2();
        const badToken_dummy3 = await badToken.dummy3();
        console.log();
        console.log("goodToken_dummy1 =", goodToken_dummy1);
        console.log("goodToken_dummy2 =", goodToken_dummy2);
        console.log("goodToken_dummy3 =", goodToken_dummy3.toString(16));
        console.log("badToken_dummy1 =", badToken_dummy1);
        console.log("badToken_dummy2 =", badToken_dummy2);
        console.log("badToken_dummy3 =", badToken_dummy3.toString(16));
    });
});

And a couple of dummy contracts that the test above relies on:

contract GoodToken {
    address public dummy1;
    address public dummy2;
    uint256 public dummy3;
    function detectTransferRestriction(address x, address y, uint256 z) public {
        dummy1 = x;
        dummy2 = y;
        dummy3 = z;
    }
}

contract BadToken {
    address public dummy1;
    address public dummy2;
    uint256 public dummy3;
    function someOtherFunc(address x, address y, uint256 z) public {
        dummy1 = x;
        dummy2 = y;
        dummy3 = z;
    }
}

I believe that in solc 0.5.x or higher, you can do it in a single Solidity line (no assembly involved).


An update following your request (in a comment) on how this can be done off-chain:

const FUNC_SIGNATURE = "detectTransferRestriction(address,address,uint256)";
const funcSelector = web3.sha3(FUNC_SIGNATURE).slice(2,10); // Truffle v4.x / Web3 v0.x
const funcSelector = web3.utils.keccak256(FUNC_SIGNATURE).slice(2,10); // Truffle v5.x / Web3 v1.x
const bytecode = await web3.eth.getCode(tokenContract.address);
console.log(bytecode.includes(funcSelector));
Related Topic