ERC721

ERC721 代币标准是 非同质化代币 的规范,或者更通俗地说:NFTs。 token::erc721::ERC721Component 提供了 Starknet 上 Cairo 中 EIP-721 的近似实现。

用法

使用 Contracts for Cairo,构造一个 ERC721 合约需要集成 ERC721ComponentSRC5Component。 合约还应设置构造函数以初始化代币的名称、符号和接口支持。 这是一个基本合约的例子:

#[starknet::contract]
mod MyNFT {
    use openzeppelin_introspection::src5::SRC5Component;
    use openzeppelin_token::erc721::{ERC721Component, ERC721HooksEmptyImpl};
    use starknet::ContractAddress;

    component!(path: ERC721Component, storage: erc721, event: ERC721Event);
    component!(path: SRC5Component, storage: src5, event: SRC5Event);

    // ERC721 Mixin
    #[abi(embed_v0)]
    impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl<ContractState>;
    impl ERC721InternalImpl = ERC721Component::InternalImpl<ContractState>;

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

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

    #[constructor]
    fn constructor(
        ref self: ContractState,
        recipient: ContractAddress
    ) {
        let name = "MyNFT";
        let symbol = "NFT";
        let base_uri = "https://api.example.com/v1/";
        let token_id = 1;

        self.erc721.initializer(name, symbol, base_uri);
        self.erc721.mint(recipient, token_id);
    }
}

接口

以下接口表示 Cairo ERC721Component Contracts 的完整 ABI。 该接口包括 IERC721 标准接口和可选的 IERC721Metadata 接口。

为了支持较旧的代币部署,如 双重接口 中所述,该组件还包括以 camelCase 编写的接口实现。

#[starknet::interface]
pub trait ERC721ABI {
    // IERC721
    fn balance_of(account: ContractAddress) -> u256;
    fn owner_of(token_id: u256) -> ContractAddress;
    fn safe_transfer_from(
        from: ContractAddress,
        to: ContractAddress,
        token_id: u256,
        data: Span<felt252>
    );
    fn transfer_from(from: ContractAddress, to: ContractAddress, token_id: u256);
    fn approve(to: ContractAddress, token_id: u256);
    fn set_approval_for_all(operator: ContractAddress, approved: bool);
    fn get_approved(token_id: u256) -> ContractAddress;
    fn is_approved_for_all(owner: ContractAddress, operator: ContractAddress) -> bool;

    // IERC721Metadata
    fn name() -> ByteArray;
    fn symbol() -> ByteArray;
    fn token_uri(token_id: u256) -> ByteArray;

    // IERC721CamelOnly
    fn balanceOf(account: ContractAddress) -> u256;
    fn ownerOf(tokenId: u256) -> ContractAddress;
    fn safeTransferFrom(
        from: ContractAddress,
        to: ContractAddress,
        tokenId: u256,
        data: Span<felt252>
    );
    fn transferFrom(from: ContractAddress, to: ContractAddress, tokenId: u256);
    fn setApprovalForAll(operator: ContractAddress, approved: bool);
    fn getApproved(tokenId: u256) -> ContractAddress;
    fn isApprovedForAll(owner: ContractAddress, operator: ContractAddress) -> bool;

    // IERC721MetadataCamelOnly
    fn tokenURI(tokenId: u256) -> ByteArray;
}

ERC721 兼容性

虽然 Starknet 与 EVM 不兼容,但此实现旨在尽可能接近 ERC721 标准。 但是,此实现确实包括一些显着差异,例如:

  • interface_ids 是硬编码的,并由构造函数初始化。 硬编码值来自 Starknet 的选择器计算。 参见 内省 文档。

  • safe_transfer_from 在 Cairo 中只能表示为单个函数,而不是 EIP721 中声明的两个函数,因为 Cairo 目前不可能进行函数重载。 两个函数之间的区别在于接受 data 作为参数。 默认情况下,safe_transfer_from 接受 data 参数,该参数被解释为 Span<felt252>。 如果未使用 data,只需传递一个空数组。

  • ERC721 利用 SRC5 在 Starknet 上声明和查询接口支持,而不是 Ethereum 的 EIP165SRC5 的设计类似于 OpenZeppelin 的 ERC165Storage

  • IERC721Receiver 兼容合约根据 Starknet 选择器返回硬编码的接口 ID(而不是 Solidity 中的选择器计算)。

代币转移

这个库包括 transfer_fromsafe_transfer_from 来转移 NFTs。 如果使用 transfer_from调用者有责任确认接收者能够接收 NFTs,否则它们可能会永久丢失。 safe_transfer_from 方法通过查询接收者合约的接口支持来减轻此风险。

使用 safe_transfer_from 可以防止损失,但调用者必须理解这增加了一个外部调用,可能会产生重入漏洞。

接收代币

为了确保非账户合约可以安全地接受 ERC721 代币,该合约必须实现 IERC721Receiver 接口。 接收者合约还必须实现 SRC5 接口,如前所述,该接口支持接口自省。

IERC721Receiver

#[starknet::interface]
pub trait IERC721Receiver {
    fn on_erc721_received(
        operator: ContractAddress,
        from: ContractAddress,
        token_id: u256,
        data: Span<felt252>
    ) -> felt252;
}

实现 IERC721Receiver 接口会暴露 on_erc721_received 方法。 当调用诸如 safe_transfer_fromsafe_mint 等安全方法时,它们会调用接收者合约的 on_erc721_received 方法,该方法*必须*返回 IERC721Receiver 接口 ID。 否则,交易将失败。

有关如何计算接口 ID 的信息,请参阅 计算接口 ID

创建代币接收者合约

Contracts for Cairo IERC721ReceiverImpl 已经为安全代币转移返回正确的接口 ID。 要将 IERC721Receiver 接口集成到合约中,只需在实现中包含 ABI 嵌入指令,并将 initializer 添加到合约的构造函数中。 这是一个简单的代币接收者合约的例子:

#[starknet::contract]
mod MyTokenReceiver {
    use openzeppelin_introspection::src5::SRC5Component;
    use openzeppelin_token::erc721::ERC721ReceiverComponent;
    use starknet::ContractAddress;

    component!(path: ERC721ReceiverComponent, storage: erc721_receiver, event: ERC721ReceiverEvent);
    component!(path: SRC5Component, storage: src5, event: SRC5Event);

    // ERC721Receiver Mixin
    #[abi(embed_v0)]
    impl ERC721ReceiverMixinImpl = ERC721ReceiverComponent::ERC721ReceiverMixinImpl<ContractState>;
    impl ERC721ReceiverInternalImpl = ERC721ReceiverComponent::InternalImpl<ContractState>;

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

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

    #[constructor]
    fn constructor(ref self: ContractState) {
        self.erc721_receiver.initializer();
    }
}

存储 ERC721 URIs

在 Cairo v0.2.5 之前,代币 URIs 以前存储为单个字段元素。 现在,ERC721Component 仅将基本 URI 存储为 ByteArray,并且完整的代币 URI 作为基本 URI 和通过 token_uri 方法的代币 ID 的 ByteArray 连接返回。 此设计镜像了 OpenZeppelin 的 ERC721 的默认 Solidity 实现