Zum Hauptinhalt springen

Standard Interface Detection

Status: Final

Standard Interface Detection

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 CBC-165
  4. How to detect if a contract implements any given interface

Motivation

For some "standard interfaces" like the CBC-20 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. 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 Blockchain ABI. This is a subset of Ylem's concept of interfaces and the interface keyword definition, which also defines return types, mutability, and events.

The following code examples use syntax from Ylem 0.8.4 (or above).

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:

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

contract Selector {
function calculateSelector() public pure returns (bytes4) {
return type(ExampleInterface).interfaceId;
}
}

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 CBC-165 shall implement the following interface (referred to as CBC165.sol):

interface CBC165 {
/// @notice Query if a contract implements an interface
/// @param interfaceID The interface identifier, as specified in CBC-165
/// @dev Interface identification is specified in CBC-165. 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 0x80ada41b. Ylem calculates it from the canonical function signature supportsInterface(bytes4) using Core's SHA3-256-based function selector algorithm. It can also be obtained as type(CBC165).interfaceId or CBC165.supportsInterface.selector.

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

  • true when interfaceID is 0x80ada41b (CBC-165 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 CBC-165

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

How to Detect if a Contract Implements any Given Interface

  1. If you are not sure if the contract implements CBC-165, use the above procedure to confirm.
  2. If it does not implement CBC-165, then you will have to see what methods it uses the old-fashioned way.
  3. If it implements CBC-165 then 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 current Ylem versions and the Core Blockchain architecture.

Backward Compatibility

The mechanism described above (with 0xffffffff) should work with most contracts created before this standard to determine that they do not implement CBC-165.

Test Cases

A conforming implementation should verify at least the following cases:

  • supportsInterface(0x80ada41b) returns true.
  • supportsInterface(0xffffffff) returns false.
  • An unknown interface identifier returns false.
  • A declared supported interface identifier returns true.
  • A reverted call, an empty response, or malformed return data is treated as lack of support.

The following contract detects which interfaces other contracts implement:

contract CBC165Query {
bytes4 constant InvalidID = 0xffffffff;
bytes4 constant CBC165ID = 0x80ada41b;

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

(success, result) = noThrowCall(_contract, CBC165ID);
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) internal view returns (uint256 success, uint256 result) {
bytes4 cbc165ID = CBC165ID;

assembly {
let x := mload(0x40) // Find empty storage location using "free memory pointer"
mstore(x, cbc165ID) // Place signature at beginning 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
0x24, // Inputs are 36 bytes long
x, // Store output over input (saves space)
0x20) // Outputs are 32 bytes long

if or(iszero(success), lt(returndatasize(), 0x20)) {
success := 0
result := 0
}

if success {
result := mload(x) // Load the result
if gt(result, 1) {
success := 0
result := 0
}
}
}
}
}

Implementation

This approach uses a mapping-backed view implementation of supportsInterface. It provides constant-time lookups at the cost of storing each supported interface during contract initialization. The CBC165MappingImplementation contract is generic and reusable.

import "./CBC165.sol";

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

constructor() {
supportedInterfaces[type(CBC165).interfaceId] = true;
}

function supportsInterface(bytes4 interfaceID) external view override returns (bool) {
return interfaceID != 0xffffffff && supportedInterfaces[interfaceID];
}
}

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

contract Lisa is CBC165MappingImplementation, Simpson {
constructor() {
supportedInterfaces[type(Simpson).interfaceId] = true;
}

function is2D() external pure override returns (bool) {
return true;
}

function skinColor() external pure override returns (string memory) {
return "yellow";
}
}

The following is a storage-free pure implementation of supportsInterface. Its execution cost increases linearly with the number of supported interfaces.

import "./CBC165.sol";

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

contract Homer is CBC165, Simpson {
function supportsInterface(bytes4 interfaceID) external pure override returns (bool) {
return interfaceID != 0xffffffff
&& (interfaceID == type(CBC165).interfaceId || interfaceID == type(Simpson).interfaceId);
}

function is2D() external pure override returns (bool) {
return true;
}

function skinColor() external pure override returns (string memory) {
return "yellow";
}
}

The mapping approach trades initialization and storage costs for constant-time lookups. The storage-free approach avoids those costs, but each additional supported interface adds another comparison. Implementers should choose based on their deployment and call patterns while keeping supportsInterface below the required 30,000 energy limit.

Security Considerations

  • A contract can falsely claim support for any interface. Callers MUST NOT treat a positive response as proof that the implementation is correct or trustworthy.
  • Callers SHOULD handle reverted calls, malformed return data, and contracts that consume the available energy.
  • Implementers MUST ensure that supportsInterface(0xffffffff) always returns false.
  • Implementers should consider energy costs when interface detection is used in frequently called functions.

Conclusion

The CBC-165 standard provides a standard method for interface detection on Core Blockchain, enabling smart contracts to dynamically discover and interact with each other based on their declared interfaces.

Copyright and related rights waived via CC0.

Tags:CBC