ERC1155

The ERC1155 multi token standard is a specification for fungibility-agnostic token contracts. The ERC1155 library implements an approximation of EIP-1155 in Cairo for StarkNet.

Multi Token Standard

The distinctive feature of ERC1155 is that it uses a single smart contract to represent multiple tokens at once. This is why its balance_of function differs from ERC20’s and ERC777’s: it has an additional ID argument for the identifier of the token that you want to query the balance of.

This is similar to how ERC721 does things, but in that standard a token ID has no concept of balance: each token is non-fungible and exists or doesn’t. The ERC721 balance_of function refers to how many different tokens an account has, not how many of each. On the other hand, in ERC1155 accounts have a distinct balance for each token ID, and non-fungible tokens are implemented by simply minting a single one of them.

This approach leads to massive gas savings for projects that require multiple tokens. Instead of deploying a new contract for each token type, a single ERC1155 token contract can hold the entire system state, reducing deployment costs and complexity.

Usage

Using Contracts for Cairo, constructing an ERC1155 contract requires integrating both ERC1155Component and SRC5Component. The contract should also set up the constructor to initialize the token’s URI and interface support. Here’s an example of a basic contract:

#[starknet::contract]
mod MyERC1155 {
    use openzeppelin::introspection::src5::SRC5Component;
    use openzeppelin::token::erc1155::{ERC1155Component, ERC1155HooksEmptyImpl};
    use starknet::ContractAddress;

    component!(path: ERC1155Component, storage: erc1155, event: ERC1155Event);
    component!(path: SRC5Component, storage: src5, event: SRC5Event);

    // ERC1155 Mixin
    #[abi(embed_v0)]
    impl ERC1155MixinImpl = ERC1155Component::ERC1155MixinImpl<ContractState>;
    impl ERC1155InternalImpl = ERC1155Component::InternalImpl<ContractState>;

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

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

    #[constructor]
    fn constructor(
        ref self: ContractState,
        token_uri: ByteArray,
        recipient: ContractAddress,
        token_ids: Span<u256>,
        values: Span<u256>
    ) {
        self.erc1155.initializer(token_uri);
        self
            .erc1155
            .batch_mint_with_acceptance_check(recipient, token_ids, values, array![].span());
    }
}

Interface

The following interface represents the full ABI of the Contracts for Cairo ERC1155Component. The interface includes the IERC1155 standard interface and the optional IERC1155MetadataURI interface together with ISRC5.

To support older token deployments, as mentioned in Dual interfaces, the component also includes implementations of the interface written in camelCase.

#[starknet::interface]
pub trait ERC1155ABI {
    // IERC1155
    fn balance_of(account: ContractAddress, token_id: u256) -> u256;
    fn balance_of_batch(
        accounts: Span<ContractAddress>, token_ids: Span<u256>
    ) -> Span<u256>;
    fn safe_transfer_from(
        from: ContractAddress,
        to: ContractAddress,
        token_id: u256,
        value: u256,
        data: Span<felt252>
    );
    fn safe_batch_transfer_from(
        from: ContractAddress,
        to: ContractAddress,
        token_ids: Span<u256>,
        values: Span<u256>,
        data: Span<felt252>
    );
    fn is_approved_for_all(
        owner: ContractAddress, operator: ContractAddress
    ) -> bool;
    fn set_approval_for_all(operator: ContractAddress, approved: bool);

    // IERC1155MetadataURI
    fn uri(token_id: u256) -> ByteArray;

    // ISRC5
    fn supports_interface(interface_id: felt252) -> bool;

    // IERC1155Camel
    fn balanceOf(account: ContractAddress, tokenId: u256) -> u256;
    fn balanceOfBatch(
        accounts: Span<ContractAddress>, tokenIds: Span<u256>
    ) -> Span<u256>;
    fn safeTransferFrom(
        from: ContractAddress,
        to: ContractAddress,
        tokenId: u256,
        value: u256,
        data: Span<felt252>
    );
    fn safeBatchTransferFrom(
        from: ContractAddress,
        to: ContractAddress,
        tokenIds: Span<u256>,
        values: Span<u256>,
        data: Span<felt252>
    );
    fn isApprovedForAll(owner: ContractAddress, operator: ContractAddress) -> bool;
    fn setApprovalForAll(operator: ContractAddress, approved: bool);
}

ERC1155 Compatibility

Although Starknet is not EVM compatible, this implementation aims to be as close as possible to the ERC1155 standard but some differences can still be found, such as:

  • The optional data argument in both safe_transfer_from and safe_batch_transfer_from is implemented as Span<felt252>.

  • IERC1155Receiver compliant contracts must implement SRC5 and register the IERC1155Receiver interface ID.

  • IERC1155Receiver::on_erc1155_received must return that interface ID on success.

Batch operations

Because all state is held in a single contract, it is possible to operate over multiple tokens in a single transaction very efficiently. The standard provides two functions, balance_of_batch and safe_batch_transfer_from, that make querying multiple balances and transferring multiple tokens simpler and less gas-intensive. We also have safe_transfer_from for non-batch operations.

In the spirit of the standard, we’ve also included batch operations in the non-standard functions, such as batch_mint_with_acceptance_check.

While safe_transfer_from and safe_batch_transfer_from prevent loss by checking the receiver can handle the tokens, this yields execution to the receiver which can result in a reentrant call.

Receiving tokens

In order to be sure a non-account contract can safely accept ERC1155 tokens, said contract must implement the IERC1155Receiver interface. The recipient contract must also implement the SRC5 interface which supports interface introspection.

IERC1155Receiver

#[starknet::interface]
pub trait IERC1155Receiver {
    fn on_erc1155_received(
        operator: ContractAddress,
        from: ContractAddress,
        token_id: u256,
        value: u256,
        data: Span<felt252>
    ) -> felt252;
    fn on_erc1155_batch_received(
        operator: ContractAddress,
        from: ContractAddress,
        token_ids: Span<u256>,
        values: Span<u256>,
        data: Span<felt252>
    ) -> felt252;
}

Implementing the IERC1155Receiver interface exposes the on_erc1155_received and on_erc1155_batch_received methods. When safe_transfer_from and safe_batch_transfer_from are called, they invoke the recipient contract’s on_erc1155_received or on_erc1155_batch_received methods respectively which must return the IERC1155Receiver 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 ERC1155ReceiverComponent already returns the correct interface ID for safe token transfers. To integrate the IERC1155Receiver interface into a contract, simply include the ABI embed directive to the implementations 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::erc1155::ERC1155ReceiverComponent;
    use starknet::ContractAddress;

    component!(path: ERC1155ReceiverComponent, storage: erc1155_receiver, event: ERC1155ReceiverEvent);
    component!(path: SRC5Component, storage: src5, event: SRC5Event);

    // ERC1155Receiver Mixin
    #[abi(embed_v0)]
    impl ERC1155ReceiverMixinImpl = ERC1155ReceiverComponent::ERC1155ReceiverMixinImpl<ContractState>;
    impl ERC1155ReceiverInternalImpl = ERC1155ReceiverComponent::InternalImpl<ContractState>;

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

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

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