Tagged

Writing and deploying your first ERC20 token on Starknet

Learn how to build your first Cairo smart contract!

Darlington Nnam
Jan 31, 2023

This article is part of an extended content series we’re working on to help bring developers into the Starknet ecosystem.

By the end of this article, you’ll understand what an ERC20 token is and how to create one from scratch.

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:

  1. name() -> (name: felt) — which returns the name of the token
  2. symbol() -> (symbol: felt) — which returns the token’s symbol
  3. decimals() -> (decimals: felt) — which returns the token’s decimals
  4. totalSupply() -> (totalSupply: Uint256) — which returns the token’s total supply.
  5. balanceOf(account: felt) -> (balance: Uint256) — which queries and returns the balance of a particular account.
  6. allowance(owner: felt, spender: felt) -> (remaining: Uint256) — which queries and returns the allowance assigned to a spender by an owner.
  7. transfer(recipient: felt, amount: Uint256) -> (success: felt) — which transfers a certain amount of tokens from a sender to the specified recipient.
  8. 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.
  9. 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 Scarb

As always, we will be using the Scarb package manager for development. If you still don’t have Scarb installed at this point, refer to the documentation here. To get started, we will need to initialize a new project. Let’s call our project “argentERC20”.

To do this, run:

Scarb new argentERC20

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.

Creating a new file titled

ERC20 Interface

Inside our new file, we’ll begin with the #[starknet::interface] attribute, which we'll use to specify the ERC20 interface implemented by our contract.

use starknet::ContractAddress;

#[starknet::interface]
trait IERC20<TContractState> {
    fn get_name(self: @TContractState) -> felt252;
    fn get_symbol(self: @TContractState) -> felt252;
    fn get_decimals(self: @TContractState) -> u8;
    fn get_total_supply(self: @TContractState) -> u256;
    fn balance_of(self: @TContractState, account: ContractAddress) -> u256;
    fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256;
    fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256);
    fn transfer_from(
        ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256
    );
    fn approve(ref self: TContractState, spender: ContractAddress, amount: u256);
    fn increase_allowance(ref self: TContractState, spender: ContractAddress, added_value: u256);
    fn decrease_allowance(
        ref self: TContractState, spender: ContractAddress, subtracted_value: u256
    );
}

Imports

Next up, we’ll introduce the #[starknet::contract] attribute, which specifies that our file contains code for a Starknet contract.

After we’ve done that, we’ll create a new module “ERC20” and begin by importing all the necessary library functions we’ll need for our contract.

#[starknet::contract]
mod ERC20 {
    use starknet::ContractAddress;
    use starknet::get_caller_address;
    use starknet::contract_address_const;
    use zeroable::Zeroable;
    use super::IERC20;
}

Storage variables

Going forward, we'll need to define our storage variables. With the new Cairo syntax, storage structs must be specified using the `#[storage]` attribute:

   #[storage]
    struct Storage {
        name: felt252,
        symbol: felt252,
        decimals: u8,
        total_supply: u256,
        balances: LegacyMap<ContractAddress, u256>,
        allowances: LegacyMap<(ContractAddress, ContractAddress), u256>,
    }

Events

Our contracts will definitely need to emit certain events such as `Approval`, and `Transfer`. To do this we’ll need first to specify all the events to be emitted in an enum called `Event`, with custom data types.

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        Approval: Approval,
        Transfer: Transfer
    }

Finally, we’ll create these structs, with the members being the variables to be emitted:

   #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        Transfer: Transfer,
        Approval: Approval,
    }

    #[derive(Drop, starknet::Event)]
    struct Transfer {
        from: ContractAddress,
        to: ContractAddress,
        value: u256,
    }

    #[derive(Drop, starknet::Event)]
    struct Approval {
        owner: ContractAddress,
        spender: ContractAddress,
        value: u256,
    }

Writing the contract

Constructor

For our ERC20 token, we need to initialize certain variables on deployment, such as the namesymbol, decimals, total_supply and balance of the recipient. To do this, our contract must implement a constructor:

    #[constructor]
    fn constructor(
        ref self: ContractState,
        _name: felt252,
        _symbol: felt252,
        _decimals: u8,
        _initial_supply: u256,
        recipient: ContractAddress
    ) {
        self.name.write(_name);
        self.symbol.write(_symbol);
        self.decimals.write(_decimals);
        self.total_supply.write(_initial_supply);
        self.balances.write(recipient, _initial_supply);

        self.emit(
            Transfer {
                from: contract_address_const::<0>(),
                to: recipient,
                value: _initial_supply
            }
        );
    }

As you can see from the snippet above, the constructor function takes in a reference to a `self` parameter which points to the contract’s storage, a `_name` parameter representing the name of our token, a `_symbol` parameter representing the token symbol, the `_decimals` parameter representing the token decimals, and the `_initial_supply` representing the initial supply of the token. These are initialized within the constructor body.

Contract Implementation

With the new Cairo syntax, all public functions are defined within an implementation block, and required to subscribe to a certain Interface which we specified at the top of the contract. 

We are also going to be specifying the `external[v0]` attribute to inform the compiler that the functions contained within this implementation block are public/external functions.

#[external(v0)]
    impl IERC20Impl of IERC20<ContractState> {
        fn get_name(self: @ContractState) -> felt252 {
            self.name.read()
        }

        fn get_symbol(self: @ContractState) -> felt252 {
            self.symbol.read()
        }

        fn get_decimals(self: @ContractState) -> u8 {
            self.decimals.read()
        }

        fn get_total_supply(self: @ContractState) -> u256 {
            self.total_supply.read()
        }

        fn balance_of(self: @ContractState, account: ContractAddress) -> u256 {
            self.balances.read(account)
        }

        fn allowance(self: @ContractState, owner: ContractAddress, spender: ContractAddress) -> u256 {
            self.allowances.read((owner, spender))
        }

        fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) {
            let caller = get_caller_address();
            self.transfer_helper(caller, recipient, amount);
        }

        fn transfer_from(ref self: ContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256) {
            let caller = get_caller_address();
            self.spend_allowance(sender, caller, amount);
            self.transfer_helper(sender, recipient, amount);
        }

        fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) {
            let caller = get_caller_address();
            self.approve_helper(caller, spender, amount);
        }

        fn increase_allowance(ref self: ContractState, spender: ContractAddress, added_value: u256) {
            let caller = get_caller_address();
            self.approve_helper(caller, spender, self.allowances.read((caller, spender)) + added_value);
        }

        fn decrease_allowance(ref self: ContractState, spender: ContractAddress, subtracted_value: u256) {
            let caller = get_caller_address();
            self.approve_helper(caller, spender, self.allowances.read((caller, spender)) - subtracted_value);
        }
    }

Finally, if you take a good look at the code above, you’ll notice we delegated the logic for certain functions to a helper function. We’ll also need to write out these helper functions, within an implementation block, but since they are not public functions we are going to exclude the `external[v0]` attribute.

We are also going to let the compiler automatically generate the interface trait by using the`generate_trait`attribute.

#[generate_trait]
    impl HelperImpl of HelperTrait {
        fn transfer_helper(ref self: ContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256) {
            assert(!sender.is_zero(), 'transfer from 0');
            assert(!recipient.is_zero(), 'transfer to 0');

            self.balances.write(sender, self.balances.read(sender) - amount);
            self.balances.write(recipient, self.balances.read(recipient) + amount);

            self.emit(
                Transfer {
                    from: sender,
                    to: recipient,
                    value: amount,
                }
            );
        }

        fn approve_helper(ref self: ContractState, owner: ContractAddress, spender: ContractAddress, amount: u256) {
            assert(!owner.is_zero(), 'approve from 0');
            assert(!spender.is_zero(), 'approve to 0');

            self.allowances.write((owner, spender), amount);

            self.emit(
                Approval {
                    owner,
                    spender,
                    value: amount,
                }
            )
        }

        fn spend_allowance(ref self: ContractState, owner: ContractAddress, spender: ContractAddress, amount: u256) {
            let current_allowance = self.allowances.read((owner, spender));
            let ONES_MASK = 0xffffffffffffffffffffffffffffffff_u128;
            let is_unlimited_allowance = current_allowance.low == ONES_MASK && current_allowance.high == ONES_MASK;

            if !is_unlimited_allowance {
                self.approve_helper(owner, spender, current_allowance - amount);
            }
        }
    }

Conclusion

Having gotten to this point, congratulations! You just completed your first ERC20 Starknet contract. You can find the full source code in the repo 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 Careers

Related 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

Own It

We use 🍪 cookies to personalise your experience on Argent. Privacy Policy

Accept

HQ London, made with ❤️ across Europe