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. The token::erc20::ERC20 contract implements 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.

Interface

The following interface represents the full ABI of the Contracts for Cairo ERC20 preset. The interface includes the IERC20 standard interface as well as non-standard functions like increase_allowance. To support older deployments of ERC20, as mentioned in Dual interfaces, ERC20 also includes the previously defined functions written in camelCase.

trait ERC20ABI {
    // IERC20
    fn name() -> felt252;
    fn symbol() -> felt252;
    fn decimals() -> u8;
    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;

    // Non-standard
    fn increase_allowance(spender: ContractAddress, added_value: u256) -> bool;
    fn decrease_allowance(spender: ContractAddress, subtracted_value: u256) -> bool;

    // Camel case compatibility
    fn totalSupply() -> u256;
    fn balanceOf(account: ContractAddress) -> u256;
    fn transferFrom(
        sender: ContractAddress, recipient: ContractAddress, amount: u256
    ) -> bool;
    fn increaseAllowance(spender: ContractAddress, addedValue: u256) -> bool;
    fn decreaseAllowance(spender: ContractAddress, subtractedValue: u256) -> bool;
}

ERC20 compatibility

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

  • Cairo short strings are used to simulate name and symbol because Cairo does not yet include native string support.

  • The contract 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.

Usage

The following example uses unsafe_new_contract_state to access another contract’s state. Although this is useful to use them as modules, it’s considered unsafe because storage members could clash among used contracts if not reviewed carefully. Extensibility will be revisited after Components are introduced.

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

#[starknet::contract]
mod MyToken {
    use starknet::ContractAddress;
    use openzeppelin::token::erc20::ERC20;

    #[storage]
    struct Storage {}

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

        let mut unsafe_state = ERC20::unsafe_new_contract_state();
        ERC20::InternalImpl::initializer(ref unsafe_state, name, symbol);
        ERC20::InternalImpl::_mint(ref unsafe_state, recipient, initial_supply);
    }

    #[external(v0)]
    impl MyTokenImpl of IERC20<ContractState> {
        fn name(self: @ContractState) -> felt252 {
            let mut unsafe_state = ERC20::unsafe_new_contract_state();
            ERC20::ERC20Impl::name(@unsafe_state)
        }

        (...)
    }
}

In order for the MyToken contract to extend the ERC20 contract, it utilizes the unsafe_new_contract_state. The unsafe contract state allows access to ERC20’s implementations. With this access, the constructor first calls the initializer to set the token name and symbol. The constructor then calls _mint to create a fixed supply of tokens.

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

In the external implementation, notice that MyTokenImpl is an implementation of IERC20. This ensures that the external implementation will include all of the methods defined in the interface.

Customizing decimals

Cairo, like Solidity, does not support floating-point numbers. To get around this limitation, ERC20 offers 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 implementation of decimals 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.

The static approach

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

#[external(v0)]
impl MyTokenImpl of IERC20<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.

#[starknet::contract]
mod MyToken {
    use starknet::ContractAddress;
    use openzeppelin::token::erc20::ERC20;

    #[storage]
    struct Storage {
        // The decimals value is stored locally
        _decimals: u8,
    }

    #[constructor]
    fn constructor(
        ref self: ContractState,
        decimals: u8
    ) {
        // Call the internal function that writes decimals to storage
        self._set_decimals(decimals);

        // Initialize ERC20
        let name = 'MyToken';
        let symbol = 'MTK';

        let mut unsafe_state = ERC20::unsafe_new_contract_state();
        ERC20::InternalImpl::initializer(ref unsafe_state, name, symbol);
    }

    /// This is a standalone function for brevity.
    /// It's recommended to create an implementation of IERC20
    /// to ensure that the contract exposes the entire ERC20 interface.
    /// See the previous example.
    #[external(v0)]
    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 stored in the local contract’s storage because this variable does not exist in the Contracts for Cairo library. It’s important to include the correct logic in the exposed decimals method and to NOT use the Contracts for Cairo decimals implementation in this specific case. The library’s decimals implementation does not read from storage and will return 18.