ERC20

ERC20 代币标准是一种针对 同质化代币 的规范,这是一种所有单位彼此完全相等的代币类型。 token::erc20::ERC20Component 提供了在 Cairo 中用于 Starknet 的 EIP-20 的近似实现。

Contracts v0.7.0 之前,ERC20 合约存储并从存储中读取 decimals;但是,此实现返回静态的 18。 如果升级的小数位数不是 18 的旧 ERC20 合约,则升级后的合约*必须*使用自定义的 decimals 实现。 请参阅 自定义小数位数 指南。

用法

使用 Contracts for Cairo 构建 ERC20 合约需要设置构造函数并实例化代币实现。 如下所示:

#[starknet::contract]
mod MyToken {
    use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl, DefaultConfig};
    use starknet::ContractAddress;

    component!(path: ERC20Component, storage: erc20, event: ERC20Event);

    // ERC20 Mixin
    #[abi(embed_v0)]
    impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl<ContractState>;
    impl ERC20InternalImpl = ERC20Component::InternalImpl<ContractState>;

    #[storage]
    struct Storage {
        #[substorage(v0)]
        erc20: ERC20Component::Storage
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        #[flat]
        ERC20Event: ERC20Component::Event
    }

    #[constructor]
    fn constructor(
        ref self: ContractState,
        initial_supply: u256,
        recipient: ContractAddress
    ) {
        let name = "MyToken";
        let symbol = "MTK";

        self.erc20.initializer(name, symbol);
        self.erc20.mint(recipient, initial_supply);
    }
}

MyToken 使用 embed 指令集成了 ERC20ImplERC20MetadataImpl,该指令将实现标记为合约中的外部实现。 虽然 ERC20MetadataImpl 是可选的,但通常建议包含它,因为绝大多数 ERC20 代币都提供元数据方法。 上面的示例还包括 ERC20InternalImpl 实例。 这允许合约的构造函数初始化合约并创建代币的初始供应量。

有关 ERC20 代币机制的更完整指南,请参阅 创建 ERC20 供应量

接口

以下接口表示 Contracts for Cairo ERC20Component 的完整 ABI。 该接口包括 IERC20 标准接口以及可选的 IERC20Metadata

为了支持较早的代币部署,如 双重接口 中所述,该组件还包括以 camelCase 编写的接口的实现。

#[starknet::interface]
pub trait ERC20ABI {
    // IERC20
    fn total_supply() -> u256;
    fn balance_of(account: ContractAddress) -> u256;
    fn allowance(owner: ContractAddress, spender: ContractAddress) -> u256;
    fn transfer(recipient: ContractAddress, amount: u256) -> bool;
    fn transfer_from(
        sender: ContractAddress, recipient: ContractAddress, amount: u256
    ) -> bool;
    fn approve(spender: ContractAddress, amount: u256) -> bool;

    // IERC20Metadata
    fn name() -> ByteArray;
    fn symbol() -> ByteArray;
    fn decimals() -> u8;

    // IERC20Camel
    fn totalSupply() -> u256;
    fn balanceOf(account: ContractAddress) -> u256;
    fn transferFrom(
        sender: ContractAddress, recipient: ContractAddress, amount: u256
    ) -> bool;
}

ERC20 兼容性

虽然 Starknet 与 EVM 不兼容,但此组件旨在尽可能接近 ERC20 代币标准。 但是,仍然可以找到一些值得注意的差异,例如:

  • ByteArray 类型用于表示 Cairo 中的字符串。

  • 该组件提供了一个 双重接口,它支持 snake_case 和 camelCase 方法,而不是像 Solidity 中那样只支持 camelCase。

  • transfertransfer_fromapprove 永远不会返回与 true 不同的任何值,因为它们会在任何错误时恢复。

  • 函数选择器在 CairoSolidity 之间以不同的方式计算。

自定义小数位数

像 Solidity 一样,Cairo 不支持https://en.wikipedia.org//wiki/Floating-point_arithmetic[浮点数]。 为了解决这个限制,ERC20 代币合约可以提供一个 decimals 字段,该字段向外部接口(钱包、交易所等)传达应如何显示代币。 例如,假设一个代币的 decimals 值为 3,并且代币总供应量为 1234。 外部接口会将代币供应量显示为 1.234。 但是,在实际合约中,供应量仍然是整数 1234。 换句话说,小数位数域绝不会改变实际的算术,因为所有操作仍然在整数上执行。

大多数合约使用 18 位小数,甚至有人提议将其设为强制性(请参阅 EIP 讨论)。

静态方法 (SRC-107)

Contracts for Cairo ERC20 组件利用 SRC-107 来允许使用静态且可配置的小数位数。 要使用默认的 18 位小数,您只需导入 DefaultConfig 实现即可:

#[starknet::contract]
mod MyToken {
    // Importing the DefaultConfig implementation would make decimals 18 by default.
    // 导入 DefaultConfig 实现将默认使小数位数为 18。
    use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl, DefaultConfig};
    use starknet::ContractAddress;

    component!(path: ERC20Component, storage: erc20, event: ERC20Event);

    #[abi(embed_v0)]
    impl ERC20Impl = ERC20Component::ERC20Impl<ContractState>;
    #[abi(embed_v0)]
    impl ERC20CamelOnlyImpl = ERC20Component::ERC20CamelOnlyImpl<ContractState>;
    impl ERC20InternalImpl = ERC20Component::InternalImpl<ContractState>;

    (...)
}

要自定义此值,您可以在合约本地实现 ImmutableConfig 特征。 以下示例展示了如何将小数位数设置为 6

mod MyToken {
    use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl};
    use starknet::ContractAddress;

    component!(path: ERC20Component, storage: erc20, event: ERC20Event);

    #[abi(embed_v0)]
    impl ERC20Impl = ERC20Component::ERC20Impl<ContractState>;
    #[abi(embed_v0)]
    impl ERC20CamelOnlyImpl = ERC20Component::ERC20CamelOnlyImpl<ContractState>;
    impl ERC20InternalImpl = ERC20Component::InternalImpl<ContractState>;

    (...)

    // Custom implementation of the ERC20Component ImmutableConfig.
    // ERC20Component ImmutableConfig 的自定义实现。
    impl ERC20ImmutableConfig of ERC20Component::ImmutableConfig {
        const DECIMALS: u8 = 6;
    }
}

存储方法

对于更复杂的场景,例如工厂部署具有不同小数位数值的多个代币,灵活的解决方案可能更合适。

请注意,在这种情况下我们没有使用 MixinImpl 或 DefaultConfig,因为我们需要自定义 IERC20Metadata 实现。
#[starknet::contract]
mod MyToken {
    use openzeppelin_token::erc20::interface;
    use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl};
    use starknet::ContractAddress;

    component!(path: ERC20Component, storage: erc20, event: ERC20Event);

    #[abi(embed_v0)]
    impl ERC20Impl = ERC20Component::ERC20Impl<ContractState>;
    #[abi(embed_v0)]
    impl ERC20CamelOnlyImpl = ERC20Component::ERC20CamelOnlyImpl<ContractState>;
    impl ERC20InternalImpl = ERC20Component::InternalImpl<ContractState>;

    #[storage]
    struct Storage {
        #[substorage(v0)]
        erc20: ERC20Component::Storage,
        // The decimals value is stored locally
        // 小数位数的值存储在本地
        decimals: u8,
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        #[flat]
        ERC20Event: ERC20Component::Event,
    }

    #[constructor]
    fn constructor(
        ref self: ContractState, decimals: u8, initial_supply: u256, recipient: ContractAddress,
    ) {
        // Call the internal function that writes decimals to storage
        // 调用将小数位数写入存储的内部函数
        self._set_decimals(decimals);

        // Initialize ERC20
        // 初始化 ERC20
        let name = "MyToken";
        let symbol = "MTK";

        self.erc20.initializer(name, symbol);
        self.erc20.mint(recipient, initial_supply);
    }

    #[abi(embed_v0)]
    impl ERC20CustomMetadataImpl of interface::IERC20Metadata<ContractState> {
        fn name(self: @ContractState) -> ByteArray {
            self.erc20.ERC20_name.read()
        }

        fn symbol(self: @ContractState) -> ByteArray {
            self.erc20.ERC20_symbol.read()
        }

        fn decimals(self: @ContractState) -> u8 {
            self.decimals.read()
        }
    }

    #[generate_trait]
    impl InternalImpl of InternalTrait {
        fn _set_decimals(ref self: ContractState, decimals: u8) {
            self.decimals.write(decimals);
        }
    }
}

此合约需要构造函数中的 decimals 参数,并使用内部函数将小数位数写入存储。 请注意,decimals 状态变量必须在合约的存储中定义,因为此变量不存在于 OpenZeppelin Contracts for Cairo 提供的组件中。 在这种特定情况下,重要的是包括自定义 ERC20 元数据实现,而不是使用 Contracts for Cairo ERC20MetadataImpl,因为 decimals 方法将始终返回 18