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.
|
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.
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
andapprove
will never return anything different fromtrue
because they will revert on any error. -
Function selectors are calculated differently between Cairo and Solidity.
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. |
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:
#[external(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);
}
#[external(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
.