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
andsymbol
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
andapprove
will never return anything different fromtrue
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
.