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 is 0. Note that URIs cannot exceed 31 characters at this time. See Storing ERC721 URIs.

  • interface_ids 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 accepting data as an argument. safe_transfer_from by default accepts the data argument. If data is not used, simply pass an empty array.

  • safe_transfer_from is implemented such that the optional data argument mimics bytes. 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.