ERC1155

ERC1155 多代币标准是一个 与同质化无关 代币合约的规范。 ERC1155 库在 Cairo 中实现了 StarkNet 的 EIP-1155 的近似。

多代币标准

ERC1155 的显著特点是它使用单个智能合约一次性表示多个代币。 这就是为什么它的 balance_of 函数与 ERC20 和 ERC777 的不同:它有一个额外的 ID 参数,用于标识你要查询余额的代币。

这与 ERC721 的工作方式类似,但在该标准中,代币 ID 没有余额的概念:每个代币都是非同质化的,存在或不存在。 ERC721 的 balance_of 函数指的是一个账户有多少不同的代币,而不是每个代币有多少。 另一方面,在 ERC1155 中,账户对每个代币 ID 都有不同的余额,而非同质化代币的实现方式是简单地铸造其中的一个。

这种方法为需要多种代币的项目节省了大量的 Gas。 无需为每种代币类型部署新的合约,单个 ERC1155 代币合约可以持有整个系统状态,从而降低部署成本和复杂性。

用法

使用 Contracts for Cairo,构建 ERC1155 合约需要集成 ERC1155ComponentSRC5Component。 该合约还应设置构造函数以初始化代币的 URI 和接口支持。 这是一个基本合约的示例:

#[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());
    }
}

接口

以下接口表示 Contracts for Cairo ERC1155Component 的完整 ABI。 该接口包括 IERC1155 标准接口和可选的 IERC1155MetadataURI 接口以及 ISRC5

为了支持较早的代币部署,如 双重接口 中所述,该组件还包括以 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 兼容性

虽然 Starknet 与 EVM 不兼容,但此实现旨在尽可能接近 ERC1155 标准,但仍然可以发现一些差异,例如:

  • safe_transfer_fromsafe_batch_transfer_from 中的可选 data 参数实现为 Span<felt252>

  • 符合 IERC1155Receiver 的合约必须实现 SRC5 并注册 IERC1155Receiver 接口 ID。

  • IERC1155Receiver::on_erc1155_received 必须在成功时返回该接口 ID。

批量操作

由于所有状态都保存在单个合约中,因此可以在单个事务中非常有效地对多个代币进行操作。 该标准提供了两个函数 balance_of_batchsafe_batch_transfer_from,它们使查询多个余额和转移多个代币更简单且 Gas 消耗更少。 我们还有用于非批量操作的 safe_transfer_from

本着标准的精神,我们还在非标准函数中包含了批量操作,例如 batch_mint_with_acceptance_check

虽然 safe_transfer_fromsafe_batch_transfer_from 通过检查接收者是否可以处理代币来防止损失,但这会将执行权交给接收者,这可能会导致 重入调用

接收代币

为了确保非账户合约可以安全地接受 ERC1155 代币,该合约必须实现 IERC1155Receiver 接口。 接收者合约还必须实现支持接口自省的 SRC5 接口。

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;
}

实现 IERC1155Receiver 接口会公开 on_erc1155_receivedon_erc1155_batch_received 方法。 当调用 safe_transfer_fromsafe_batch_transfer_from 时,它们分别调用接收者合约的 on_erc1155_receivedon_erc1155_batch_received 方法,这些方法*必须*返回 IERC1155Receiver 接口 ID。 否则,事务将失败。

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

创建代币接收器合约

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

#[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();
    }
}