Starknet Standard Interface Detection

The main goal of SRC5 is to support interoperability, allowing contracts to interact with each other with consistency and simplicity, knowing what behavior to expect from the target.

With the New Syntax interfaces, we have an important issue if try to support Ids of interfaces containing generic types (besides the TContractState that is compiler-generated):

If we have this interface:

trait IMyContract<TContractState, TNumber> {
    fn foo(self: @TContractState, some: TNumber) -> felt252;

A contract implementing it like this:

impl IMyContractImpl of IMyContract<ContractState, felt252> {
        fn foo(self: @ContractState, some: felt252) -> felt252 {

Would break if someone tries to call the external foo method with a u256 instead of a felt252 as the second param, because the actual external method of the contract is not generic, but specific to the type set in the Impl block. Even worst, the call could not break if the calldata is serde-compatible, with potential unexpected bad outcomes.

With this in mind, I think we should NOT ALLOW these interfaces in the standard, so even when they are language supported, they won’t be SRC5 compliant.

For the SNIP terminology I will call these interfaces (with generic types) Interface Blueprints, and the ones without it just Interfaces, because an Interface Blueprint can translate into multiple interfaces for different contracts regarding the input types that will be accepted in the actual external methods of the contract (that can’t be generic at the time being). This naming convention is mostly for updating the SNIP specification.

Summary: I think Interface Blueprints should be NOT ALLOWED, while Interfaces SHOULD.

Interested in opinions and potential other solutions/thoughts around this.

I updated the SNIP to reflect that the Blueprint is not the actual interface used for computing the Id, and interfaces don’t include the ContractState type, as that is not part of the inputs the real external function is expecting (is just a facade for handling self-storage, is not encoded in calldata).

Last SNIP update here.

With the latest changes about the syntax evolving
Do you think it is useful to add self in the computation?

On second thought, I’m not sure if I would include this on the interface id computation or not yet, because I don’t know if we need to encode self somehow in calldata when using call_contract_syscall for calling a contract implementing this interface. If this doesn’t need to be encoded, I wouldn’t add the self type to the interface id. Need to check with the compiler team.

Quick update:

After some discussion, it seems worth having the function output as part of the signature when computing the interface id, and this was added in the last update of the SNIP-5 accordingly.

That’s a nice work @ericnordelo and a very crucial part to ensure contract interop.

About the new syntax, it could make sense to include the self to know if it’s a reference or not. Like this two interfaces which may be close, but one is requiring a ref and the other not, we can make the difference. What do you think?

Also, can you explain the E((),()) representing a bool?

Thanks a lot, very interesting stuff.

As far as I understand there’s no encoding of self, so there is no need to represent it in the interface as it doesn’t affect interoperation.


But in that case there is no way to differentiate view fn from external fn (or even pure fn when that’ll be supported)?

The rule is to represent base types (extern types in corelib) as they are, but Enums and Structs as defined in the SNIP, even when they are located in corelib. bool is an enum defined in corelib, and that’s why is represented as such.

If Enums or Structs in corelib are modified for any reason (improvements, refactor, removals, etc…), having them represented as enums and not as core types allow the SNIP to automatically “acknowledge” the incompatibility of the new interface, by requiring a change of the function signature.

If they are treated as core types (that are, arguably, less likely to be modified), then the new interface would be represented the same even when is not compatible. bool may not be the best example, but there are other enums and structs, that can potentially change, that’s why they are treated like that basically (we could add exceptions like bool to the rule, but this would be an unnecessary overhead IMO, because this signature is just an intermediate state for the id computation).

About the new syntax, it could make sense to include the self to know if it’s a reference or not. Like this two interfaces which may be close, but one is requiring a ref and the other not, we can make the difference. What do you think?

This standard is meant to ensure that a caller contract A knows what to expect from the interface of a called contract B, but the implementation of the called method of B may differ, and the standard is not meant to avoid that.

The self param is a mechanism that restricts how a contract can implement its methods, but it doesn’t affect the interface of the method at all (when called externally). The external interface is basically what you need to pass in call_contract_syscall (and the output), and this is what we mean with encoded data.

Even when self is saying something about the method, is affecting just the local implementation, not the external interface. The call to B is exactly the same when we have self as ref or as a snapshot.

But in that case there is no way to differentiate view fn from external fn (or even pure fn when that’ll be supported)?

Those are implementation restrictions for the contract that don’t modify the public interface, so is not the goal of the standard to differentiate those. For the caller contract A, is the same thing if the method is pure or not from the interface perspective, because the call it needs to execute is the same (same function name, params types, and output type). Even if the protocol treats view and external methods differently (like solidity STATICCALL vs CALL opcodes), the interface is the same, and so it should be represented the same.

I created this tool for computing the interface ids (SRC-5 compliant) directly from Cairo source code automatically, to save the time of manual signature translation, and potential human mistakes from the process (as I made a couple). Sharing it here for visibility, as I think it may be helpful:

This SNIP is well established and has already been adopted by several implementations, but I would like to open an idea for discussion.

What I find missing in this interface detection mechanism is the ability to detect a single function, or a subset of an interface.

In some contexts, it would be useful to know if a contract I’m interacting with, has a specific function I’d like to call. This function might be part of a standard interface, amortized by a SNIP, and might not be.
Only if the contract uses SRC-5, and I know exactly which interact to look for, will I be able to find out if a specific function I need is supported.

If a contract wants to declare support for any subset of its interact, it would to compute and store the XOR for all subsets of its extended selectors.

One idea, is to use bloom filters, storing an aggregation of all extended selectors, and upon query, get a definite negative if a function is not in the interface, with some chance of a false positive result.

Appriciate your input and whether this should be moved to a separate discussion.

Interesting idea, my quick thoughts are:

If you want to match a single function outside of this context, you could try-catch with the call_contract_syscall result.

The main idea of introspection is to check whether a contract behaves in an expected way specified in an interface following some SNIP. This interface id gives context to the methods, and is not just whether the contract has the entry point or not, is also what you can expect from it.

From this I don’t think this would get too much usability, but you may have better examples that the ones coming to my mind.