10

Given a valid contract address how can I check if the contract implemented a certain interface i.e Ownable?

1

5 Answers 5

13

ERC165 tackles this problem but it cannot be used for older ERCs. For instance, most ERC20 implementations don't implement it (as of Nov 2018, at least OpenZeppelin doesn't). You could try calling the supportsInterface function, but it would revert and you'd rather complicate things.

However, here's how it's defined in ERC721:

bytes4 private constant _InterfaceId_ERC721 = 0x80ac58cd;
/*
 * 0x80ac58cd ===
 *   bytes4(keccak256('balanceOf(address)')) ^
 *   bytes4(keccak256('ownerOf(uint256)')) ^
 *   bytes4(keccak256('approve(address,uint256)')) ^
 *   bytes4(keccak256('getApproved(uint256)')) ^
 *   bytes4(keccak256('setApprovalForAll(address,bool)')) ^
 *   bytes4(keccak256('isApprovedForAll(address,address)')) ^
 *   bytes4(keccak256('transferFrom(address,address,uint256)')) ^
 *   bytes4(keccak256('safeTransferFrom(address,address,uint256)')) ^
 *   bytes4(keccak256('safeTransferFrom(address,address,uint256,bytes)'))
 */

It's not guaranteed that all implementations define the interface id, but there's a good chance that ERC721s do it, since the community agreed on applying ERC165 right from the get-go. If the return value of the query below is true, then it means you have a compliant contract, otherwise just revert the transaction.

// you can call this in your contracts
IERC721(contractAddress).supportsInterface(0x80ac58cd)

Also, a useful resource for manually checking the bytes4 of a given method is 4byte.directory

0

Simply make a transaction using the supposed ABI to the contract and watch the answer. If it succeeds, do not take for assured the interface presence, but test some ancillary supposed methods in order to assess (i.e. if it answer to a totalSupply query, it does not assure you that a real totalSupply() method do exist, but if the result of the method called are consistent, you can use it).

As answered before, there it exists EIP proposal to solve this.

0

You could try to call the contract method owner() using solidity's call, staticcall or directly using asembbly, this link explains it better https://medium.com/@blockchain101/calling-the-function-of-another-contract-in-solidity-f9edfa921f4c

Solidity's call has issues with bytes and string types. Solidity's author, chriseth, recommends to avoid using it. https://github.com/ethereum/solidity/issues/2884#issuecomment-329169020 This was fixed in https://github.com/ethereum/solidity/issues/4652#issuecomment-411712839 see solidity specifications for solidity 0.5.0 https://solidity.readthedocs.io/en/v0.5.0/050-breaking-changes.html#semantic-and-syntactic-changes

Soliditi's StaticCall is the same as call but it would throw an error if tries to change state, this is great to avoid reentrancy attacks https://github.com/ethereum/EIPs/pull/214/files

0

Ok I'm trying to implement @rick-park solution (in Solidity): call and look at the result, in case there is some.

(the rationale behind: I'm providing some functionalities to msg.sender in my contract. I would like any contract owner() to use these functionalities on behalf of their owned contract. So I'm calling

try IOwnable(someAddr).owner() returns (address contractOwner) {
  // do some stuff with contractOwner
  // ...
} catch {
  // some other logic if needed
}

My IOwnable interface is very simple:

interface IOwnable {
    function owner() external view returns (address);
}

The problem is when I test that contract with HardHat, I get some nasty:

Error: call revert exception [ See: https://links.ethers.org/v5-errors-CALL_EXCEPTION ]
(method="myMethod()",
 data="0x",
 errorArgs=null,
 errorName=null,
 errorSignature=null,
 reason=null,
 code=CALL_EXCEPTION,
 version=abi/5.6.4)

I was hoping my call would trigger an error, and then get caught in the catch block ... but it looks like it isn't.

Keep in mind that my first test was obviously to call IOwnable(someAddr).owner() on some test contract that did not implement ownable, to see what happens.

0

there are my lastest methods used to check if a contract has a function:

import _ from 'lodash';
import { Abi, Address, BaseError, ContractFunctionRevertedError, PublicClient } from 'viem';
import Web3 from 'web3';


/**
 * @description unable to penetrate proxy contracts
 * @param method The param need the format:```transferFrom(address,address,uint256)```
 */
export const isMethodExistA = async (web3: Web3, contractAddress: Address, method: string): Promise<boolean> => {
  try {
    const code = await web3.eth.getCode(contractAddress);
    const methodSignature = web3.eth.abi.encodeFunctionSignature(method);

    return code.indexOf(methodSignature.slice(2, methodSignature.length)) > 0;
  } catch (e: any) {
    return false;
  }
};


/**
 * @description unable to penetrate proxy contracts
 * @param method The param need the format:```{
    inputs: [],
    name: 'getOwners',
    outputs: [{ internalType: 'address[]', name: '', type: 'address[]' }],
    stateMutability: 'view',
    type: 'function'
  }```
 */
export const isMethodExistB = async (web3: Web3, contractAddress: Address, method: any): Promise<boolean> => {
  try {
    const code = await web3.eth.getCode(contractAddress);
    const methodSignature = web3.eth.abi.encodeFunctionSignature(method);

    return code.indexOf(methodSignature.slice(2, methodSignature.length)) > 0;
  } catch (e: any) {
    return false;
  }
};


/**
 * @description can penetrate proxy contracts
 * @param methodABI The param need the format:```[{
    inputs: [],
    name: 'getOwners',
    outputs: [{ internalType: 'address[]', name: '', type: 'address[]' }],
    stateMutability: 'view',
    type: 'function'
  }]```, and perform const assertion, that is ```const [] as const```
 */
export const isMethodExistC = async (
  publicClient: PublicClient,
  contractAddress: Address,
  methodABI: Abi,
  functionName: string,
  args?: any[]
) => {
  try {
    const ret = await publicClient.simulateContract({
      address: contractAddress,
      abi: methodABI,
      functionName: functionName,
      args: args,
    });
    return true;
  } catch (e: any) {
    if (e instanceof BaseError) {
      if (e.cause instanceof ContractFunctionRevertedError) {
        return true;
      }
    }
    return false;
  }
};

There are some examples using the above methods:

import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';
import Web3 from 'web3';

const web3 = new Web3('https://eth-mainnet.g.alchemy.com/v2/...');
const publicClient = createPublicClient({
  chain: mainnet,
  transport: http('https://eth-mainnet.g.alchemy.com/v2/...'),
});

isMethodExistA(
  web3,
  '0x791250f047ae1de09f2a9ab98eec91caa9bf0a2d',
  'execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)'
).then((ret) => console.log('ret:', ret));

isMethodExistB(web3, '0xd9db270c1b5e3bd161e8c8503c55ceabee709552', {
  inputs: [
    { internalType: 'address', name: 'to', type: 'address' },
    { internalType: 'uint256', name: 'value', type: 'uint256' },
    { internalType: 'bytes', name: 'data', type: 'bytes' },
    {
      internalType: 'enum Enum.Operation',
      name: 'operation',
      type: 'uint8',
    },
    { internalType: 'uint256', name: 'safeTxGas', type: 'uint256' },
    { internalType: 'uint256', name: 'baseGas', type: 'uint256' },
    { internalType: 'uint256', name: 'gasPrice', type: 'uint256' },
    { internalType: 'address', name: 'gasToken', type: 'address' },
    {
      internalType: 'address payable',
      name: 'refundReceiver',
      type: 'address',
    },
    { internalType: 'bytes', name: 'signatures', type: 'bytes' },
  ],
  name: 'execTransaction',
  outputs: [{ internalType: 'bool', name: 'success', type: 'bool' }],
  stateMutability: 'payable',
  type: 'function',
}).then((ret) => console.log('ret:', ret));

const execTransactionABI = [
  {
    inputs: [
      { internalType: 'address', name: 'to', type: 'address' },
      { internalType: 'uint256', name: 'value', type: 'uint256' },
      { internalType: 'bytes', name: 'data', type: 'bytes' },
      {
        internalType: 'enum Enum.Operation',
        name: 'operation',
        type: 'uint8',
      },
      { internalType: 'uint256', name: 'safeTxGas', type: 'uint256' },
      { internalType: 'uint256', name: 'baseGas', type: 'uint256' },
      { internalType: 'uint256', name: 'gasPrice', type: 'uint256' },
      { internalType: 'address', name: 'gasToken', type: 'address' },
      {
        internalType: 'address payable',
        name: 'refundReceiver',
        type: 'address',
      },
      { internalType: 'bytes', name: 'signatures', type: 'bytes' },
    ],
    name: 'execTransaction',
    outputs: [{ internalType: 'bool', name: 'success', type: 'bool' }],
    stateMutability: 'payable',
    type: 'function',
  },
] as const;

isMethodExistC(publicClient, '0x29469395eaf6f95920e59f858042f0e28d98a20b', execTransactionABI, 'execTransaction', [
  '0xfec7443780F800601118288dEd58D339Fd06dAC4',
  33,
  '0x4444',
  0,
  33,
  33,
  33,
  '0xfec7443780F800601118288dEd58D339Fd06dAC4',
  '0xfec7443780F800601118288dEd58D339Fd06dAC4',
  '0x233',
])
  .then((exist) => console.log('execTransaction exist:', exist))
  .catch((e) => console.log('execTransaction err:', e.message));

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.