This article is part of an extended content series we’re working on to help bring developers into the Starknet ecosystem.
Previously, we explained how to create the ultimate Cairo development environment using Protostar, Argent X, and StarkScan.
Once you’ve set that up, you’re ready to write and deploy your first ERC20 token to Starknet!
By the end of this article, you’ll understand what an ERC20 token is and how to create one using OpenZeppelin’s library.
WTF is an ERC20 token?
ERC20 tokens are fungible, meaning that they are replaceable by another of their token. This is just how money works. A $20 bill in one wallet holds the same value and can be used in the same way as a different $20 bill.
Fabian Vogelsteller and Vitalik Buterin proposed the ERC20 standard to create a set of universal standards for fungible tokens. Before the ERC20 standard, every token on Ethereum was different, meaning wallets had to write custom code to support them.
An ERC20 comprises the following major methods:
- name() -> (name: felt) — which returns the name of the token
- symbol() -> (symbol: felt) — which returns the token’s symbol
- decimals() -> (decimals: felt) — which returns the token’s decimals
- totalSupply() -> (totalSupply: Uint256) — which returns the token’s total supply.
- balanceOf(account: felt) -> (balance: Uint256) — which queries and returns the balance of a particular account.
- allowance(owner: felt, spender: felt) -> (remaining: Uint256) — which queries and returns the allowance assigned to a spender by an owner.
- transfer(recipient: felt, amount: Uint256) -> (success: felt) — which transfers a certain amount of tokens from a sender to the specified recipient.
- transferFrom(sender: felt, recipient: felt, amount: Uint256) -> (success: felt) — which allows a spender to transfer a certain amount of tokens allowed to be spent by the owner/sender to the specified recipient.
- approve(spender: felt, amount: Uint256) -> (success: felt) — which approves a spender to spend a certain amount of tokens from the owner’s wallet.
Setting up Protostar Environment
Now we’re familiar with what an ERC20 token is, we can get started with writing and deploying are ERC20 token.
You’ll need to have Protostar installed. If you don’t have Protostar installed, or you want to learn more about it, we recommend our article on creating a Cairo Development Environment, as it’s essential for building on Starknet.
Once you’re ready, we will need to initialize a new project. Let’s call our project “argentERC20”.
To do this, run:
protostar init
There will be a further request for the project’s name and the library’s directory name. This is needed to successfully initialize your project.
Creating a new file
We will create a new file called ERC20.cairo in our src folder. This is where we’ll be writing our contract codes.

Imports
In our new file, we’ll begin by specifying the %lang starknet directive to specify that our file contains codes for a Starknet contract, similar to how we do pragma solidity for solidity contracts.
Then, we will import all necessary library functions we need to use in our contract.
%lang starknet from starkware.cairo.common.cairo_builtins import HashBuiltin from starkware.cairo.common.uint256 import Uint256 from starkware.cairo.common.bool import TRUE from cairo_contracts.src.openzeppelin.token.erc20.library import ERC20
As you can see from the code snippet above, we first import the HashBuiltin, Uint256, and TRUE functions from Starkware’s official cairo-lang library, then import the ERC20 namespace from Openzeppelin’s ERC20 library.
The HashBuiltin was imported because it's needed for Pedersen hashing related operations, the UINT256 provides a means to create a Uint256 struct from two felts, and the TRUE is a boolean representation of 1.
Finally, we import the ERC20 namespace, which makes all of Openzeppelin’s internal functions accessible from our contract.
Openzeppelin Library
We’ve mentioned the Openzeppelin Library repeatedly during this article, but if you are new to smart contract development, you might wonder what the Openzeppelin library is.
Openzeppelin’s library is a collection of modular, reusable, secure smart contracts for Ethereum, Starknet, and other networks, written in their respective language.
Thanks to the hard work of the Openzeppelin team, we can now easily build an ERC20 token by simply importing and using their libraries within our custom contract.
The ERC20 Token Interface
An interface is a list of a contract’s functions definitions without implementations. It helps describe a contract’s functionality and is primarily used for calling a contract from an external one.
We’re going to look at the ERC20 token’s interface to understand further the methods we will need to implement while building our token:
%lang starknet from starkware.cairo.common.uint256 import Uint256 @contract_interface namespace IERC20 { func name() -> (name: felt) { } func symbol() -> (symbol: felt) { } func decimals() -> (decimals: felt) { } func totalSupply() -> (totalSupply: Uint256) { } func balanceOf(account: felt) -> (balance: Uint256) { } func allowance(owner: felt, spender: felt) -> (remaining: Uint256) { } func transfer(recipient: felt, amount: Uint256) -> (success: felt) { } func transferFrom(sender: felt, recipient: felt, amount: Uint256) -> (success: felt) { } func approve(spender: felt, amount: Uint256) -> (success: felt) { } }
Constructor Logic
A constructor is mainly used to initialize certain state variables on contract deployment.
To create a constructor in Cairo, you’d use the @constructor decorator.
For our ERC20 token, we want to initialize variables such as the name, symbol, decimals, and totalSupply on deployment.
To do this, our contract must implement the following constructor:
@constructor func constructor{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( _name: felt, _symbol: felt, _decimals: felt, initialSupply: Uint256, recipient: felt ){ ERC20.initializer(_name, _symbol, _decimals); ERC20._mint(recipient, initialSupply); return (); }
As the code snippet above shows, our constructor function must be named constructor and accept 5 arguments [_name, _symbol, _decimals, initialSupply, recipient].
We also call the initializer internal function by using the ERC20 namespace we imported from Openzeppelin while passing the required function arguments [name, symbol, and decimals]. The initializer function creates/instantiates a new ERC20 token with 0 total supply.
To create a fixed total supply on deployment, we call the _mint function passing in the function arguments [recipient, initialSupply]. The _mint function mints the initialSupply provided to the recipient address.
Functions in Cairo
Having implemented our constructor logic, we can now implement other functions necessary for our token to conform to the ERC20 standard, but before we proceed, let’s take out a few lines to differentiate between the two major types of functions in Cairo.
- External functions — External functions are functions that change the state of the blockchain and are created using the @external decorator.
- View functions — View functions are getter functions. They do not alter the state of the blockchain and are created using the @view decorator.
Implementing other functions
1. Name
The name function is a view function that simply returns the token’s name when queried. In the function’s logic, we simply invoke the name method in Openzeppelin’s contract using the ERC20 namespace and then return the result we get.
@view func name{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> (name: felt) { let (name) = ERC20.name(); return (name,); }
2. Symbol
The symbol function is a view function that returns the token’s symbol when queried.
@view func symbol{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> (symbol: felt) { let (symbol) = ERC20.symbol(); return (symbol,); }
3. Decimals
The decimals function is a view function that returns the token’s decimals when queried.
@view func decimals{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> (decimals: felt) { let (decimals) = ERC20.decimals(); return (decimals,); }
4. Total supply
The totalSupply function is a view function that returns the token’s total supply.
@view func totalSupply{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> (totalSupply: Uint256) { let (totalSupply) = ERC20.total_supply(); return (totalSupply,); }
5. BalanceOf
The BalanceOf function is a view function that returns the total amount owned by a particular account when queried. Like every other function in this contract, it also makes a call to the internal balance_of function in Openzeppelin’s library, but this time passing in an argument [account], which is the account whose balance is to be queried.
@view func balanceOf{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(account: felt) -> (balance: Uint256) { let (balance) = ERC20.balance_of(account); return (balance,); }
6. Allowance
This is a view function that queries and returns the amount of tokens assigned to a spender by an owner.
It takes in two arguments [owner, spender].
@view func allowance{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(owner: felt, spender: felt) -> (remaining: Uint256) { let (allowance) = ERC20.allowance(owner, spender); return (allowance,); }
7. Transfer
The transfer function is an external function that takes two arguments [recipient, amount] and initiates a transfer from the sender to the specified recipient.
@external func transfer{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( recipient: felt, amount: Uint256 ) -> (success: felt) { ERC20.transfer(recipient, amount); return (TRUE,); }
8. TransferFrom
The transferFrom function is an external function that allows a spender to transfer a certain amount of tokens allowed to be spent by the owner/sender to the specified recipient.
It takes in three arguments [sender, recipient, amount].
@external func transferFrom{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( sender: felt, recipient: felt, amount: Uint256 ) -> (success: felt) { ERC20.transfer_from(sender, recipient, amount); return (TRUE,); }
9. Approve
The approve function is also an external function that approves a spender to spend a certain amount of tokens from the owner’s wallet.
It takes in two arguments [spender, amount].
@external func approve{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( spender: felt, amount: Uint256 ) -> (success: felt) { ERC20.approve(spender, amount); return (TRUE,); }
And congratulations, you just completed your first ERC20 contract!
You can find the complete contract code here.
Deployment
Finally, we need to deploy our contract to Starknet to interact with it.
Following the usual steps, we will first build/compile, then declare, and lastly deploy.
Building your ERC20 token
Before building your contracts, ensure to specify the correct path to your contract in your protostar.toml, and to enable Protostar to locate your lib folder, which contains your Openzeppelin contracts, add the snippet:
cairo-path = ["lib"]
Your protostar.toml file should look like this:
[project] protostar-version = "0.9.1" lib-path = "lib" cairo-path = ["lib"] [contracts] ERC20 = ["src/ERC20.cairo"]
To build your contract, simply run the following:
protostar build

Declaring your ERC20 token
Before executing the “declare” command, you should set the private key associated with the specified account address in a file or in the terminal.
To set your private key in the terminal, run the command:
export PROTOSTAR_ACCOUNT_PRIVATE_KEY=[YOUR PRIVATE KEY HERE]
Do not share your private key with anyone. Not even Argent. It should be for your eyes only.
To declare your contract, simply run the command:
protostar declare ./build/ERC20.json --network testnet --account 0x0691622bBFD29e835bA4004e7425A4e9630840EbD11c5269DE51C16774585b16 --max-fee auto

Deploying your ERC20 token
Finally, we need to call the “deploy” command passing in our contract class hash to deploy our contract.
protostar deploy 0x04dae654c7b6707667a178729b512d61494fe590ab4accc46923d6409b97e617 --network testnet --account 0x0691622bBFD29e835bA4004e7425A4e9630840EbD11c5269DE51C16774585b16 --max-fee auto --inputs 71959616777844 4280903 18 10000 0 0x0691622bBFD29e835bA4004e7425A4e9630840EbD11c5269DE51C16774585b16

You’ve deployed your first ERC20 token to Starknet!
Congratulations! You just wrote and deployed your first Cairo contract on Starknet.
To interact with the deployed contract, check Starkscan here.
If you have any questions as regards this, reach out to me @0xdarlington, I’d love to help you build on Starknet with Argent X.
For more developer resources, follow us across our socials:
Twitter — @argentHq
Engineering Twitter — @argentDeveloper
LinkedIn — @argentHq
Youtube — @argentHQ
Interested in the topic? Join us!
We’re always looking for outstanding engineers to help us pioneer better UX and security in crypto. We work remotely across Europe.
Argent CareersRelated Blogs
Getting started with StarkNet
The StarkNet ecosystem is exploding and here's how you can get involved
Understanding The Universal Deployer Contract And Deploying your Contracts through Argent X
Deploying contracts on Starknet using the Universal deployer contract
Part I: WTF is Account Abstraction
Learn why it’s a game changer for crypto's adoption