Migrating ERC165 to SRC5

In the smart contract ecosystem, having the ability to query if a contract supports a given interface is an extremely important feature. The initial introspection design for Contracts for Cairo before version v0.7.0 followed Ethereum’s EIP-165. Since the Cairo language evolved introducing native types, we needed an introspection solution tailored to the Cairo ecosystem: the SNIP-5 standard. SNIP-5 allows interface ID calculations to use Cairo types and the Starknet keccak (sn_keccak) function. For more information on the decision, see the Starknet Shamans proposal or the Dual Introspection Detection discussion.

How to migrate

Migrating from ERC165 to SRC5 involves four major steps:

  1. Integrate SRC5 into the contract.

  2. Register SRC5 IDs.

  3. Add a migrate function to apply introspection changes.

  4. Upgrade the contract and call migrate.

The following guide will go through the steps with examples.

Component integration

The first step is to integrate the necessary components into the new contract. The contract should include the new introspection mechanism, SRC5Component. It should also include the InitializableComponent which will be used in the migrate function. Here’s the setup:

#[starknet::contract]
mod MigratingContract {
    use openzeppelin_introspection::src5::SRC5Component;
    use openzeppelin_security::initializable::InitializableComponent;

    component!(path: SRC5Component, storage: src5, event: SRC5Event);
    component!(path: InitializableComponent, storage: initializable, event: InitializableEvent);

    // SRC5
    #[abi(embed_v0)]
    impl SRC5Impl = SRC5Component::SRC5Impl<ContractState>;
    impl SRC5InternalImpl = SRC5Component::InternalImpl<ContractState>;

    // Initializable
    impl InitializableInternalImpl = InitializableComponent::InternalImpl<ContractState>;

    #[storage]
    struct Storage {
        #[substorage(v0)]
        src5: SRC5Component::Storage,
        #[substorage(v0)]
        initializable: InitializableComponent::Storage
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        #[flat]
        SRC5Event: SRC5Component::Event,
        #[flat]
        InitializableEvent: InitializableComponent::Event
    }
}

Interface registration

To successfully migrate ERC165 to SRC5, the contract needs to register the interface IDs that the contract supports with SRC5.

For this example, let’s say that this contract supports the IERC721 and IERC721Metadata interfaces. The contract should implement an InternalImpl and add a function to register those interfaces like this:

#[starknet::contract]
mod MigratingContract {
    use openzeppelin_token::erc721::interface::{IERC721_ID, IERC721_METADATA_ID};

    (...)

    #[generate_trait]
    impl InternalImpl of InternalTrait {
        // Register SRC5 interfaces
        fn register_src5_interfaces(ref self: ContractState) {
            self.src5.register_interface(IERC721_ID);
            self.src5.register_interface(IERC721_METADATA_ID);
        }
    }
}

Since the new contract integrates SRC5Component, it can leverage SRC5’s register_interface function to register the supported interfaces.

Migration initializer

Next, the contract should define and expose a migration function that will invoke the register_src5_interfaces function. Since the migrate function will be publicly callable, it should include some sort of Access Control so that only permitted addresses can execute the migration. Finally, migrate should include a reinitialization check to ensure that it cannot be called more than once.

If the original contract implemented Initializable at any point and called the initialize method, the InitializableComponent will not be usable at this time. Instead, the contract can take inspiration from InitializableComponent and create its own initialization mechanism.
#[starknet::contract]
mod MigratingContract {
    (...)

    #[external(v0)]
    fn migrate(ref self: ContractState) {
        // WARNING: Missing Access Control mechanism. Make sure to add one

        // WARNING: If the contract ever implemented `Initializable` in the past,
        // this will not work. Make sure to create a new initialization mechanism
        self.initializable.initialize();

        // Register SRC5 interfaces
        self.register_src5_interfaces();
    }
}

Execute migration

Once the new contract is prepared for migration and rigorously tested, all that’s left is to migrate! Simply upgrade the contract and then call migrate.