Skip to content
This repository was archived by the owner on Aug 1, 2025. It is now read-only.

Commit c372633

Browse files
authored
feat: add ERC20 example (#100)
* feat: add ERC20 example * fix: broken cairo format check quickfix
1 parent 3e4c697 commit c372633

File tree

7 files changed

+253
-0
lines changed

7 files changed

+253
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
target
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "erc20"
3+
version = "0.1.0"
4+
5+
[dependencies]
6+
starknet = ">=2.3.0-rc0"
7+
8+
[[target.starknet-contract]]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
mod token;
2+
3+
#[cfg(test)]
4+
mod tests;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
mod tests { // TODO
2+
}
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
use starknet::ContractAddress;
2+
3+
// ANCHOR: interface
4+
#[starknet::interface]
5+
trait IERC20<TContractState> {
6+
fn get_name(self: @TContractState) -> felt252;
7+
fn get_symbol(self: @TContractState) -> felt252;
8+
fn get_decimals(self: @TContractState) -> u8;
9+
fn get_total_supply(self: @TContractState) -> felt252;
10+
fn balance_of(self: @TContractState, account: ContractAddress) -> felt252;
11+
fn allowance(
12+
self: @TContractState, owner: ContractAddress, spender: ContractAddress
13+
) -> felt252;
14+
fn transfer(ref self: TContractState, recipient: ContractAddress, amount: felt252);
15+
fn transfer_from(
16+
ref self: TContractState,
17+
sender: ContractAddress,
18+
recipient: ContractAddress,
19+
amount: felt252
20+
);
21+
fn approve(ref self: TContractState, spender: ContractAddress, amount: felt252);
22+
fn increase_allowance(ref self: TContractState, spender: ContractAddress, added_value: felt252);
23+
fn decrease_allowance(
24+
ref self: TContractState, spender: ContractAddress, subtracted_value: felt252
25+
);
26+
}
27+
// ANCHOR_END: interface
28+
29+
// ANCHOR: erc20
30+
#[starknet::contract]
31+
mod erc20 {
32+
use zeroable::Zeroable;
33+
use starknet::get_caller_address;
34+
use starknet::contract_address_const;
35+
use starknet::ContractAddress;
36+
37+
#[storage]
38+
struct Storage {
39+
name: felt252,
40+
symbol: felt252,
41+
decimals: u8,
42+
total_supply: felt252,
43+
balances: LegacyMap::<ContractAddress, felt252>,
44+
allowances: LegacyMap::<(ContractAddress, ContractAddress), felt252>,
45+
}
46+
47+
#[event]
48+
#[derive(Drop, starknet::Event)]
49+
enum Event {
50+
Transfer: Transfer,
51+
Approval: Approval,
52+
}
53+
#[derive(Drop, starknet::Event)]
54+
struct Transfer {
55+
from: ContractAddress,
56+
to: ContractAddress,
57+
value: felt252,
58+
}
59+
#[derive(Drop, starknet::Event)]
60+
struct Approval {
61+
owner: ContractAddress,
62+
spender: ContractAddress,
63+
value: felt252,
64+
}
65+
66+
mod Errors {
67+
const APPROVE_FROM_ZERO: felt252 = 'ERC20: approve from 0';
68+
const APPROVE_TO_ZERO: felt252 = 'ERC20: approve to 0';
69+
const TRANSFER_FROM_ZERO: felt252 = 'ERC20: transfer from 0';
70+
const TRANSFER_TO_ZERO: felt252 = 'ERC20: transfer to 0';
71+
const BURN_FROM_ZERO: felt252 = 'ERC20: burn from 0';
72+
const MINT_TO_ZERO: felt252 = 'ERC20: mint to 0';
73+
}
74+
75+
#[constructor]
76+
fn constructor(
77+
ref self: ContractState,
78+
recipient: ContractAddress,
79+
name: felt252,
80+
decimals: u8,
81+
initial_supply: felt252,
82+
symbol: felt252
83+
) {
84+
self.name.write(name);
85+
self.symbol.write(symbol);
86+
self.decimals.write(decimals);
87+
self.mint(recipient, initial_supply);
88+
}
89+
90+
#[external(v0)]
91+
impl IERC20Impl of super::IERC20<ContractState> {
92+
fn get_name(self: @ContractState) -> felt252 {
93+
self.name.read()
94+
}
95+
96+
fn get_symbol(self: @ContractState) -> felt252 {
97+
self.symbol.read()
98+
}
99+
100+
fn get_decimals(self: @ContractState) -> u8 {
101+
self.decimals.read()
102+
}
103+
104+
fn get_total_supply(self: @ContractState) -> felt252 {
105+
self.total_supply.read()
106+
}
107+
108+
fn balance_of(self: @ContractState, account: ContractAddress) -> felt252 {
109+
self.balances.read(account)
110+
}
111+
112+
fn allowance(
113+
self: @ContractState, owner: ContractAddress, spender: ContractAddress
114+
) -> felt252 {
115+
self.allowances.read((owner, spender))
116+
}
117+
118+
fn transfer(ref self: ContractState, recipient: ContractAddress, amount: felt252) {
119+
let sender = get_caller_address();
120+
self._transfer(sender, recipient, amount);
121+
}
122+
123+
fn transfer_from(
124+
ref self: ContractState,
125+
sender: ContractAddress,
126+
recipient: ContractAddress,
127+
amount: felt252
128+
) {
129+
let caller = get_caller_address();
130+
self.spend_allowance(sender, caller, amount);
131+
self._transfer(sender, recipient, amount);
132+
}
133+
134+
fn approve(ref self: ContractState, spender: ContractAddress, amount: felt252) {
135+
let caller = get_caller_address();
136+
self.approve_helper(caller, spender, amount);
137+
}
138+
139+
fn increase_allowance(
140+
ref self: ContractState, spender: ContractAddress, added_value: felt252
141+
) {
142+
let caller = get_caller_address();
143+
self
144+
.approve_helper(
145+
caller, spender, self.allowances.read((caller, spender)) + added_value
146+
);
147+
}
148+
149+
fn decrease_allowance(
150+
ref self: ContractState, spender: ContractAddress, subtracted_value: felt252
151+
) {
152+
let caller = get_caller_address();
153+
self
154+
.approve_helper(
155+
caller, spender, self.allowances.read((caller, spender)) - subtracted_value
156+
);
157+
}
158+
}
159+
160+
#[generate_trait]
161+
impl InternalImpl of InternalTrait {
162+
fn _transfer(
163+
ref self: ContractState,
164+
sender: ContractAddress,
165+
recipient: ContractAddress,
166+
amount: felt252
167+
) {
168+
assert(!sender.is_zero(), Errors::TRANSFER_FROM_ZERO);
169+
assert(!recipient.is_zero(), Errors::TRANSFER_TO_ZERO);
170+
self.balances.write(sender, self.balances.read(sender) - amount);
171+
self.balances.write(recipient, self.balances.read(recipient) + amount);
172+
self.emit(Transfer { from: sender, to: recipient, value: amount });
173+
}
174+
175+
fn spend_allowance(
176+
ref self: ContractState,
177+
owner: ContractAddress,
178+
spender: ContractAddress,
179+
amount: felt252
180+
) {
181+
let allowance = self.allowances.read((owner, spender));
182+
self.allowances.write((owner, spender), allowance - amount);
183+
}
184+
185+
fn approve_helper(
186+
ref self: ContractState,
187+
owner: ContractAddress,
188+
spender: ContractAddress,
189+
amount: felt252
190+
) {
191+
assert(!spender.is_zero(), Errors::APPROVE_TO_ZERO);
192+
self.allowances.write((owner, spender), amount);
193+
self.emit(Approval { owner, spender, value: amount });
194+
}
195+
196+
fn mint(ref self: ContractState, recipient: ContractAddress, amount: felt252) {
197+
assert(!recipient.is_zero(), Errors::MINT_TO_ZERO);
198+
let supply = self.total_supply.read() + amount; // What can go wrong here?
199+
self.total_supply.write(supply);
200+
let balance = self.balances.read(recipient) + amount;
201+
self.balances.write(recipient, amount);
202+
self
203+
.emit(
204+
Event::Transfer(
205+
Transfer {
206+
from: contract_address_const::<0>(), to: recipient, value: amount
207+
}
208+
)
209+
);
210+
}
211+
}
212+
}
213+
// ANCHOR_END: erc20
214+
215+

src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Summary
2525
# Applications examples
2626
- [Upgradeable Contract](./ch01/upgradeable_contract.md)
2727
- [Defi Vault](./ch01/simple_vault.md)
28+
- [ERC20 Token](./ch01/erc20.md)
2829

2930
<!-- ch02 -->
3031
# Advanced concepts

src/ch01/erc20.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# ERC20 Token
2+
3+
Contracts that follow the [ERC20 Standard](https://eips.ethereum.org/EIPS/eip-20) are called ERC20 tokens. They are used to represent fungible assets.
4+
5+
To create an ERC20 conctract, it must implement the following interface:
6+
7+
```rust
8+
{{#include ../../listings/ch01-applications/erc20/src/token.cairo:interface}}
9+
```
10+
11+
In Starknet, function names should be written in *snake_case*. This is not the case in Solidity, where function names are written in *camelCase*.
12+
The Starknet ERC20 interface is therefore slightly different from the Solidity ERC20 interface.
13+
14+
Here's an implementation of the ERC20 interface in Cairo:
15+
16+
```rust
17+
{{#include ../../listings/ch01-applications/erc20/src/token.cairo:erc20}}
18+
```
19+
20+
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).
21+
22+
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 commit comments

Comments
 (0)