ERC721
用法
使用 Contracts for Cairo,构造一个 ERC721 合约需要集成 ERC721Component
和 SRC5Component
。
合约还应设置构造函数以初始化代币的名称、符号和接口支持。
这是一个基本合约的例子:
#[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_id
s 是硬编码的,并由构造函数初始化。 硬编码值来自 Starknet 的选择器计算。 参见 内省 文档。 -
safe_transfer_from
在 Cairo 中只能表示为单个函数,而不是 EIP721 中声明的两个函数,因为 Cairo 目前不可能进行函数重载。 两个函数之间的区别在于接受data
作为参数。 默认情况下,safe_transfer_from
接受data
参数,该参数被解释为Span<felt252>
。 如果未使用data
,只需传递一个空数组。 -
ERC721 利用 SRC5 在 Starknet 上声明和查询接口支持,而不是 Ethereum 的 EIP165。
SRC5
的设计类似于 OpenZeppelin 的 ERC165Storage。 -
IERC721Receiver
兼容合约根据 Starknet 选择器返回硬编码的接口 ID(而不是 Solidity 中的选择器计算)。
代币转移
这个库包括 transfer_from 和 safe_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_from 和 safe_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 实现。