将 ERC165 迁移到 SRC5

在智能合约生态系统中,能够查询合约是否支持给定接口是一项极其重要的功能。 在 v0.7.0 之前的 Cairo 合约的初始内省设计遵循 Ethereum 的 EIP-165。 由于 Cairo 语言的演进引入了原生类型,我们需要一种为 Cairo 生态系统量身定制的内省解决方案:https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-5.md[SNIP-5] 标准。 SNIP-5 允许接口 ID 计算使用 Cairo 类型和 Starknet keccak (sn_keccak) 函数。 有关该决策的更多信息,请参阅 Starknet Shamans 提案双重内省检测 讨论。

如何迁移

从 ERC165 迁移到 SRC5 涉及四个主要步骤:

  1. 将 SRC5 集成到合约中。

  2. 注册 SRC5 ID。

  3. 添加一个 migrate 函数来应用内省更改。

  4. 升级合约并调用 migrate

以下指南将通过示例介绍这些步骤。

组件集成

第一步是将必要的组件集成到新合约中。 合约应包括新的内省机制 SRC5Component。 它还应包括 InitializableComponent,该组件将在 migrate 函数中使用。 这是设置:

#[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
    }
}

接口注册

为了成功地将 ERC165 迁移到 SRC5,合约需要使用 SRC5 注册合约支持的接口 ID。

对于此示例,假设此合约支持 IERC721IERC721Metadata 接口。 合约应实现一个 InternalImpl 并添加一个函数来注册这些接口,如下所示:

#[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);
        }
    }
}

由于新合约集成了 SRC5Component,因此它可以利用 SRC5 的 register_interface 函数来注册支持的接口。

迁移初始化器

接下来,合约应该定义并公开一个迁移函数,该函数将调用 register_src5_interfaces 函数。 由于 migrate 函数将是可公开调用的,因此它应包含某种 访问控制,以便只有允许的地址才能执行迁移。 最后,migrate 应包含重新初始化检查,以确保它不能被多次调用。

如果原始合约在任何时候实现了 Initializable 并调用了 initialize 方法,则此时 InitializableComponent 将不可用。 相反,合约可以从 InitializableComponent 中获得启发并创建自己的初始化机制。
#[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();
    }
}

执行迁移

一旦新合约为迁移做好了准备并经过*严格测试*,剩下的就是迁移了! 只需升级合约,然后调用 migrate