Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ERC20 example #100

Merged
merged 2 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.