Skip to content

Commit

Permalink
feat: add ERC20 example (#100)
Browse files Browse the repository at this point in the history
* feat: add ERC20 example

* fix: broken cairo format check quickfix
  • Loading branch information
julio4 authored Oct 24, 2023
1 parent 3e4c697 commit c372633
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 0 deletions.
1 change: 1 addition & 0 deletions listings/ch01-applications/erc20/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target
8 changes: 8 additions & 0 deletions listings/ch01-applications/erc20/Scarb.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "erc20"
version = "0.1.0"

[dependencies]
starknet = ">=2.3.0-rc0"

[[target.starknet-contract]]
4 changes: 4 additions & 0 deletions listings/ch01-applications/erc20/src/lib.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
mod token;

#[cfg(test)]
mod tests;
2 changes: 2 additions & 0 deletions listings/ch01-applications/erc20/src/tests.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mod tests { // TODO
}
215 changes: 215 additions & 0 deletions listings/ch01-applications/erc20/src/token.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
use starknet::ContractAddress;

// ANCHOR: interface
#[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) -> felt252;
fn balance_of(self: @TContractState, account: ContractAddress) -> felt252;
fn allowance(
self: @TContractState, owner: ContractAddress, spender: ContractAddress
) -> felt252;
fn transfer(ref self: TContractState, recipient: ContractAddress, amount: felt252);
fn transfer_from(
ref self: TContractState,
sender: ContractAddress,
recipient: ContractAddress,
amount: felt252
);
fn approve(ref self: TContractState, spender: ContractAddress, amount: felt252);
fn increase_allowance(ref self: TContractState, spender: ContractAddress, added_value: felt252);
fn decrease_allowance(
ref self: TContractState, spender: ContractAddress, subtracted_value: felt252
);
}
// ANCHOR_END: interface

// ANCHOR: erc20
#[starknet::contract]
mod erc20 {
use zeroable::Zeroable;
use starknet::get_caller_address;
use starknet::contract_address_const;
use starknet::ContractAddress;

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

#[event]
#[derive(Drop, starknet::Event)]
enum Event {
Transfer: Transfer,
Approval: Approval,
}
#[derive(Drop, starknet::Event)]
struct Transfer {
from: ContractAddress,
to: ContractAddress,
value: felt252,
}
#[derive(Drop, starknet::Event)]
struct Approval {
owner: ContractAddress,
spender: ContractAddress,
value: felt252,
}

mod Errors {
const APPROVE_FROM_ZERO: felt252 = 'ERC20: approve from 0';
const APPROVE_TO_ZERO: felt252 = 'ERC20: approve to 0';
const TRANSFER_FROM_ZERO: felt252 = 'ERC20: transfer from 0';
const TRANSFER_TO_ZERO: felt252 = 'ERC20: transfer to 0';
const BURN_FROM_ZERO: felt252 = 'ERC20: burn from 0';
const MINT_TO_ZERO: felt252 = 'ERC20: mint to 0';
}

#[constructor]
fn constructor(
ref self: ContractState,
recipient: ContractAddress,
name: felt252,
decimals: u8,
initial_supply: felt252,
symbol: felt252
) {
self.name.write(name);
self.symbol.write(symbol);
self.decimals.write(decimals);
self.mint(recipient, initial_supply);
}

#[external(v0)]
impl IERC20Impl of super::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) -> felt252 {
self.total_supply.read()
}

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

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

fn transfer(ref self: ContractState, recipient: ContractAddress, amount: felt252) {
let sender = get_caller_address();
self._transfer(sender, recipient, amount);
}

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

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

fn increase_allowance(
ref self: ContractState, spender: ContractAddress, added_value: felt252
) {
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: felt252
) {
let caller = get_caller_address();
self
.approve_helper(
caller, spender, self.allowances.read((caller, spender)) - subtracted_value
);
}
}

#[generate_trait]
impl InternalImpl of InternalTrait {
fn _transfer(
ref self: ContractState,
sender: ContractAddress,
recipient: ContractAddress,
amount: felt252
) {
assert(!sender.is_zero(), Errors::TRANSFER_FROM_ZERO);
assert(!recipient.is_zero(), Errors::TRANSFER_TO_ZERO);
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 spend_allowance(
ref self: ContractState,
owner: ContractAddress,
spender: ContractAddress,
amount: felt252
) {
let allowance = self.allowances.read((owner, spender));
self.allowances.write((owner, spender), allowance - amount);
}

fn approve_helper(
ref self: ContractState,
owner: ContractAddress,
spender: ContractAddress,
amount: felt252
) {
assert(!spender.is_zero(), Errors::APPROVE_TO_ZERO);
self.allowances.write((owner, spender), amount);
self.emit(Approval { owner, spender, value: amount });
}

fn mint(ref self: ContractState, recipient: ContractAddress, amount: felt252) {
assert(!recipient.is_zero(), Errors::MINT_TO_ZERO);
let supply = self.total_supply.read() + amount; // What can go wrong here?
self.total_supply.write(supply);
let balance = self.balances.read(recipient) + amount;
self.balances.write(recipient, amount);
self
.emit(
Event::Transfer(
Transfer {
from: contract_address_const::<0>(), to: recipient, value: amount
}
)
);
}
}
}
// ANCHOR_END: erc20


1 change: 1 addition & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Summary
# Applications examples
- [Upgradeable Contract](./ch01/upgradeable_contract.md)
- [Defi Vault](./ch01/simple_vault.md)
- [ERC20 Token](./ch01/erc20.md)

<!-- ch02 -->
# Advanced concepts
Expand Down
22 changes: 22 additions & 0 deletions src/ch01/erc20.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# ERC20 Token

Contracts that follow the [ERC20 Standard](https://eips.ethereum.org/EIPS/eip-20) are called ERC20 tokens. They are used to represent fungible assets.

To create an ERC20 conctract, it must implement the following interface:

```rust
{{#include ../../listings/ch01-applications/erc20/src/token.cairo:interface}}
```

In Starknet, function names should be written in *snake_case*. This is not the case in Solidity, where function names are written in *camelCase*.
The Starknet ERC20 interface is therefore slightly different from the Solidity ERC20 interface.

Here's an implementation of the ERC20 interface in Cairo:

```rust
{{#include ../../listings/ch01-applications/erc20/src/token.cairo:erc20}}
```

Play with this contract in [Remix](https://remix.ethereum.org/?#activate=Starknet&url=https://github.com/NethermindEth/StarknetByExample/blob/main/listings/ch01-applications/erc20/src/token.cairo).

There's several other implementations, such as the [Open Zeppelin](https://docs.openzeppelin.com/contracts-cairo/0.7.0/erc20) or the [Cairo By Example](https://cairo-by-example.com/examples/erc20/) ones.

0 comments on commit c372633

Please sign in to comment.