ERC20

The ERC20 token standard is a specification for fungible tokens, a type of token where all the units are exactly equal to each other. token::erc20::ERC20Component provides an approximation of EIP-20 in Cairo for Starknet.

Prior to Contracts v0.7.0, ERC20 contracts store and read decimals from storage; however, this implementation returns a static 18. If upgrading an older ERC20 contract that has a decimals value other than 18, the upgraded contract must use a custom decimals implementation. See the Customizing decimals guide.

Usage

Using Contracts for Cairo, constructing an ERC20 contract requires setting up the constructor and instantiating the token implementation. Here’s what that looks like:

#[starknet::contract]
mod MyToken {
    use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl};
    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 integrates both the ERC20Impl and ERC20MetadataImpl with the embed directive which marks the implementations as external in the contract. While the ERC20MetadataImpl is optional, it’s generally recommended to include it because the vast majority of ERC20 tokens provide the metadata methods. The above example also includes the ERC20InternalImpl instance. This allows the contract’s constructor to initialize the contract and create an initial supply of tokens.

For a more complete guide on ERC20 token mechanisms, see Creating ERC20 Supply.

Interface

The following interface represents the full ABI of the Contracts for Cairo ERC20Component. The interface includes the IERC20 standard interface as well as the optional IERC20Metadata.

To support older token deployments, as mentioned in Dual interfaces, the component also includes an implementation of the interface written in 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 compatibility

Although Starknet is not EVM compatible, this component aims to be as close as possible to the ERC20 token standard. Some notable differences, however, can still be found, such as:

  • The ByteArray type is used to represent strings in Cairo.

  • The component offers a dual interface which supports both snake_case and camelCase methods, as opposed to just camelCase in Solidity.

  • transfer, transfer_from and approve will never return anything different from true because they will revert on any error.

  • Function selectors are calculated differently between Cairo and Solidity.

Customizing decimals

Cairo, like Solidity, does not support floating-point numbers. To get around this limitation, ERC20 token contracts may offer a decimals field which communicates to outside interfaces (wallets, exchanges, etc.) how the token should be displayed. For instance, suppose a token had a decimals value of 3 and the total token supply was 1234. An outside interface would display the token supply as 1.234. In the actual contract, however, the supply would still be the integer 1234. In other words, the decimals field in no way changes the actual arithmetic because all operations are still performed on integers.

Most contracts use 18 decimals and this was even proposed to be compulsory (see the EIP discussion). The Contracts for Cairo ERC20 component includes a decimals function that returns 18 by default to save on gas fees. For those who want an ERC20 token with a configurable number of decimals, the following guide shows two ways to achieve this.

Both approaches require creating a custom implementation of the IERC20Metadata interface.

The static approach

The simplest way to customize decimals consists of returning the target value from the decimals method. For example:

#[abi(embed_v0)]
impl ERC20MetadataImpl of interface::IERC20Metadata<ContractState> {
    fn decimals(self: @ContractState) -> u8 {
        // Change the `3` below to the desired number of decimals
        3
    }

    (...)
}

The storage approach

For more complex scenarios, such as a factory deploying multiple tokens with differing values for decimals, a flexible solution might be appropriate.

Note that we are not using the MixinImpl in this case, since we need to customize the IERC20Metadata implementation.
#[starknet::contract]
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>;

    #[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
        let name = "MyToken";
        let symbol = "MTK";

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

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

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

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

This contract expects a decimals argument in the constructor and uses an internal function to write the decimals to storage. Note that the decimals state variable must be defined in the contract’s storage because this variable does not exist in the component offered by OpenZeppelin Contracts for Cairo. It’s important to include a custom ERC20 metadata implementation and NOT use the Contracts for Cairo ERC20MetadataImpl in this specific case since the decimals method will always return 18.