ERC721
The ERC721 token standard is a specification for non-fungible tokens, or more colloquially: NFTs.
token::erc721::ERC721Component
provides an approximation of EIP-721 in Cairo for Starknet.
Interface
The following interface represents the full ABI of the Contracts for Cairo ERC721Component
.
The interface includes the IERC721 standard interface and the optional IERC721Metadata interface.
To support older token deployments, as mentioned in Dual interfaces, the component also includes implementations of the interface written in camelCase.
trait IERC721ABI {
// 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() -> felt252;
fn symbol() -> felt252;
fn token_uri(token_id: u256) -> felt252;
// 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) -> felt252;
}
ERC721 compatibility
Although Starknet is not EVM compatible, this implementation aims to be as close as possible to the ERC721 standard by utilizing Cairo’s short strings to simulate name
and symbol
.
This implementation does, however, include a few notable differences such as:
-
token_uri
returns a felt252 representation of the queried token’s URI. The EIP721 standard, however, states that the return value should be of type string. If a token’s URI is not set, the returned value is0
. Note that URIs cannot exceed 31 characters at this time. See Storing ERC721 URIs. -
interface_id
s are hardcoded and initialized by the constructor. The hardcoded values derive from Starknet’s selector calculations. See the Introspection docs. -
safe_transfer_from
can only be expressed as a single function in Cairo as opposed to the two functions declared in EIP721, because function overloading is currently not possible in Cairo. The difference between both functions consists of acceptingdata
as an argument.safe_transfer_from
by default accepts thedata
argument. Ifdata
is not used, simply pass an empty array. -
safe_transfer_from
is implemented such that the optionaldata
argument mimicsbytes
. In Solidity, this means a dynamically-sized array. To be as close as possible to the standard, it accepts a dynamic array of felts. -
ERC721 utilizes SRC5 to declare and query interface support on Starknet as opposed to Ethereum’s EIP165. The design for
SRC5
is similar to OpenZeppelin’s ERC165Storage. -
IERC721Receiver
compliant contracts return a hardcoded interface ID according to Starknet selectors (as opposed to selector calculation in Solidity).
Usage
Using Contracts for Cairo, constructing an ERC721 contract requires integrating both ERC721Component
and SRC5Component
.
The contract should also set up the constructor to initialize the token’s name, symbol, and interface support.
Here’s an example of a basic contract:
#[starknet::contract]
mod MyNFT {
use openzeppelin::introspection::src5::SRC5Component;
use openzeppelin::token::erc721::ERC721Component;
use starknet::ContractAddress;
component!(path: ERC721Component, storage: erc721, event: ERC721Event);
component!(path: SRC5Component, storage: src5, event: SRC5Event);
// ERC721
#[abi(embed_v0)]
impl ERC721Impl = ERC721Component::ERC721Impl<ContractState>;
#[abi(embed_v0)]
impl ERC721MetadataImpl = ERC721Component::ERC721MetadataImpl<ContractState>;
#[abi(embed_v0)]
impl ERC721CamelOnly = ERC721Component::ERC721CamelOnlyImpl<ContractState>;
#[abi(embed_v0)]
impl ERC721MetadataCamelOnly =
ERC721Component::ERC721MetadataCamelOnlyImpl<ContractState>;
impl ERC721InternalImpl = ERC721Component::InternalImpl<ContractState>;
// SRC5
#[abi(embed_v0)]
impl SRC5Impl = SRC5Component::SRC5Impl<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 token_id = 1;
let token_uri = 'NFT_URI';
self.erc721.initializer(name, symbol);
self._mint_with_uri(recipient, token_id, token_uri);
}
#[generate_trait]
impl InternalImpl of InternalTrait {
fn _mint_with_uri(
ref self: ContractState,
recipient: ContractAddress,
token_id: u256,
token_uri: felt252
) {
// Initialize the ERC721 storage
self.erc721._mint(recipient, token_id);
// Mint the NFT to recipient and set the token's URI
self.erc721._set_token_uri(token_id, token_uri);
}
}
}
Token transfers
This library includes transfer_from and safe_transfer_from to transfer NFTs.
If using transfer_from
, the caller is responsible to confirm that the recipient is capable of receiving NFTs or else they may be permanently lost.
The safe_transfer_from
method mitigates this risk by querying the recipient contract’s interface support.
Usage of safe_transfer_from prevents loss, though the caller must understand this adds an external call which potentially creates a reentrancy vulnerability.
|
Receiving tokens
In order to be sure a non-account contract can safely accept ERC721 tokens, said contract must implement the IERC721Receiver
interface.
The recipient contract must also implement the SRC5 interface which, as described earlier, supports interface introspection.
IERC721Receiver
trait IERC721Receiver {
fn on_erc721_received(
operator: ContractAddress,
from: ContractAddress,
token_id: u256,
data: Span<felt252>
) -> felt252;
}
Implementing the IERC721Receiver
interface exposes the on_erc721_received method.
When safe methods such as safe_transfer_from and _safe_mint are called, they invoke the recipient contract’s on_erc721_received
method which must return the IERC721Receiver interface ID.
Otherwise, the transaction will fail.
For information on how to calculate interface IDs, see Computing the interface ID. |
Creating a token receiver contract
The Contracts for Cairo IERC721ReceiverImpl
already returns the correct interface ID for safe token transfers.
To integrate the IERC721Receiver
interface into a contract, simply include the ABI embed directive to the implementation and add the initializer
in the contract’s constructor .
Here’s an example of a simple token receiver contract:
#[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
#[abi(embed_v0)]
impl ERC721ReceiverImpl = ERC721ReceiverComponent::ERC721ReceiverImpl<ContractState>;
#[abi(embed_v0)]
impl ERC721ReceiverCamelImpl = ERC721ReceiverComponent::ERC721ReceiverCamelImpl<ContractState>;
impl ERC721ReceiverInternalImpl = ERC721ReceiverComponent::InternalImpl<ContractState>;
// SRC5
#[abi(embed_v0)]
impl SRC5Impl = SRC5Component::SRC5Impl<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();
}
}
Storing ERC721 URIs
Token URIs in Cairo are stored as single field elements (felt252
).
Each field element equates to 252-bits (or 31.5 bytes) which means that a token’s URI can be no longer than 31 characters.
Native string support in Cairo is currently in progress and tracked here. Once Cairo offers full string support, this will be revisited. |