CRC-3 Standard Interface Detection

Simple Summary

Creates a standard method to publish and detect what interfaces a smart contract implements.

Abstract

Herein, we standardize the following:

  1. How interfaces are identified
  2. How a contract will publish the interfaces it implements
  3. How to detect if a contract implements CRC-3
  4. How to detect if a contract implements any given interface

Motivation

For some “standard interfaces” like the CRC-1 token interface, it is sometimes useful to query whether a contract supports the interface and if yes, which version of the interface, in order to adapt the way in which the contract is to be interacted with. Specifically for CRC-1, a version identifier has already been proposed. This proposal standardizes the concept of interfaces and standardizes the identification (naming) of interfaces.

Specification

How Interfaces are Identified

For this standard, an interface is a set of function selectors as defined by the Core ABI. This a subset of Solidity’s concept of interfaces and the interface keyword definition which also defines return types, mutability and events.

We define the interface identifier as the XOR of all function selectors in the interface. This code example shows how to calculate an interface identifier:

pragma solidity ^0.4.20;

interface Solidity101 {
    function hello() external pure;
    function world(int) external pure;
}

contract Selector {
    function calculateSelector() public pure returns (bytes4) {
        Solidity101 i;
        return i.hello.selector ^ i.world.selector;
    }
}

Note: interfaces do not permit optional functions, therefore, the interface identity will not include them.

How a Contract will Publish the Interfaces it Implements

A contract that is compliant with CRC-3 shall implement the following interface (referred as CRC3.sol):

pragma solidity ^0.4.20;

interface CRC3 {
    /// @notice Query if a contract implements an interface
    /// @param interfaceID The interface identifier, as specified in CRC-3
    /// @dev Interface identification is specified in CRC-3. This function
    ///  uses less than 30,000 energy.
    /// @return `true` if the contract implements `interfaceID` and
    ///  `interfaceID` is not 0xffffffff, `false` otherwise
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

The interface identifier for this interface is 0x01ffc9a7. You can calculate this by running bytes4(keccak256('supportsInterface(bytes4)')); or using the Selector contract above.

Therefore the implementing contract will have a supportsInterface function that returns:

  • true when interfaceID is 0x01ffc9a7 (CIP3 interface)
  • false when interfaceID is 0xffffffff
  • true for any other interfaceID this contract implements
  • false for any other interfaceID

This function must return a bool and use at most 30,000 energy.

Implementation note, there are several logical ways to implement this function. Please see the example implementations and the discussion on energy usage.

How to Detect if a Contract Implements CRC-3

  1. The source contact makes a STATICCALL to the destination address with input data: 0x01ffc9a701ffc9a700000000000000000000000000000000000000000000000000000000 and energy 30,000. This corresponds to contract.supportsInterface(0x01ffc9a7).
  2. If the call fails or return false, the destination contract does not implement CRC-3.
  3. If the call returns true, a second call is made with input data 0x01ffc9a7ffffffff00000000000000000000000000000000000000000000000000000000.
  4. If the second call fails or returns true, the destination contract does not implement CRC-3.
  5. Otherwise it implements CRC-3.

How to Detect if a Contract Implements any Given Interface

  1. If you are not sure if the contract implements CRC-3, use the above procedure to confirm.
  2. If it does not implement CRC-3, then you will have to see what methods it uses the old-fashioned way.
  3. If it implements CRC-3 then just call supportsInterface(interfaceID) to determine if it implements an interface you can use.

Rationale

We tried to keep this specification as simple as possible. This implementation is also compatible with the current Solidity version.

Backwards Compatibility

The mechanism described above (with 0xffffffff) should work with most of the contracts previous to this standard to determine that they do not implement CRC-3.

Test Cases

Following is a contract that detects which interfaces other contracts implement. From @fulldecent and @jbaylina.

pragma solidity ^0.4.20;

contract CRC3Query {
    bytes4 constant InvalidID = 0xffffffff;
    bytes4 constant CRC3ID = 0x01ffc9a7;

    function doesContractImplementInterface(address _contract, bytes4 _interfaceId) external view returns (bool) {
        uint256 success;
        uint256 result;

        (success, result) = noThrowCall(_contract, CRC3ID);
        if ((success==0)||(result==0)) {
            return false;
        }

        (success, result) = noThrowCall(_contract, InvalidID);
        if ((success==0)||(result!=0)) {
            return false;
        }

        (success, result) = noThrowCall(_contract, _interfaceId);
        if ((success==1)&&(result==1)) {
            return true;
        }
        return false;
    }

    function noThrowCall(address _contract, bytes4 _interfaceId) constant internal returns (uint256 success, uint256 result) {
        bytes4 crc3ID = CRC3ID;

        assembly {
                let x := mload(0x40)               // Find empty storage location using "free memory pointer"
                mstore(x, crc3ID)                  // Place signature at begining of empty storage
                mstore(add(x, 0x04), _interfaceId) // Place first argument directly next to signature

                success := staticcall(
                                    30000,         // 30k energy
                                    _contract,     // To addr
                                    x,             // Inputs are stored at location x
                                    0x20,          // Inputs are 32 bytes long
                                    x,             // Store output over input (saves space)
                                    0x20)          // Outputs are 32 bytes long

                result := mload(x)                 // Load the result
        }
    }
}

Implementation

This approach uses a view function implementation of supportsInterface. The execution cost is 586 energy for any input. But contract initialization requires storing each interface (SSTORE is 20,000 energy). The CRC3MappingImplementation contract is generic and reusable.

pragma solidity ^0.4.20;

import "./CRC3.sol";

contract CRC3MappingImplementation is CRC3 {
    /// @dev You must not set element 0xffffffff to true
    mapping(bytes4 => bool) internal supportedInterfaces;

    function CRC3MappingImplementation() internal {
        supportedInterfaces[this.supportsInterface.selector] = true;
    }

    function supportsInterface(bytes4 interfaceID) external view returns (bool) {
        return supportedInterfaces[interfaceID];
    }
}

interface Simpson {
    function is2D() external returns (bool);
    function skinColor() external returns (string);
}

contract Lisa is CRC3MappingImplementation, Simpson {
    function Lisa() public {
        supportedInterfaces[this.is2D.selector ^ this.skinColor.selector] = true;
    }

    function is2D() external returns (bool){}
    function skinColor() external returns (string){}
}

Following is a pure function implementation of supportsInterface. The worst-case execution cost is 236 energy, but increases linearly with a higher number of supported interfaces.

pragma solidity ^0.4.20;

import "./CRC3.sol";

interface Simpson {
    function is2D() external returns (bool);
    function skinColor() external returns (string);
}

contract Homer is CRC3, Simpson {
    function supportsInterface(bytes4 interfaceID) external view returns (bool) {
        return
          interfaceID == this.supportsInterface.selector || // CRC3
          interfaceID == this.is2D.selector
                         ^ this.skinColor.selector; // Simpson
    }

    function is2D() external returns (bool){}
    function skinColor() external returns (string){}
}

With three or more supported interfaces (including CRC3 itself as a required supported interface), the mapping approach (in every case) costs less gas than the pure approach (at worst case).