From 8246c682e7602ea870d308fbde2ec2673db58fc2 Mon Sep 17 00:00:00 2001 From: julio4 Date: Fri, 20 Oct 2023 15:14:06 +0900 Subject: [PATCH 1/2] feat: add ERC20 example --- listings/ch01-applications/erc20/.gitignore | 1 + listings/ch01-applications/erc20/Scarb.toml | 8 + .../ch01-applications/erc20/src/lib.cairo | 4 + .../ch01-applications/erc20/src/tests.cairo | 2 + .../ch01-applications/erc20/src/token.cairo | 214 ++++++++++++++++++ src/SUMMARY.md | 1 + src/ch01/erc20.md | 22 ++ 7 files changed, 252 insertions(+) create mode 100644 listings/ch01-applications/erc20/.gitignore create mode 100644 listings/ch01-applications/erc20/Scarb.toml create mode 100644 listings/ch01-applications/erc20/src/lib.cairo create mode 100644 listings/ch01-applications/erc20/src/tests.cairo create mode 100644 listings/ch01-applications/erc20/src/token.cairo create mode 100644 src/ch01/erc20.md diff --git a/listings/ch01-applications/erc20/.gitignore b/listings/ch01-applications/erc20/.gitignore new file mode 100644 index 00000000..eb5a316c --- /dev/null +++ b/listings/ch01-applications/erc20/.gitignore @@ -0,0 +1 @@ +target diff --git a/listings/ch01-applications/erc20/Scarb.toml b/listings/ch01-applications/erc20/Scarb.toml new file mode 100644 index 00000000..0059aff8 --- /dev/null +++ b/listings/ch01-applications/erc20/Scarb.toml @@ -0,0 +1,8 @@ +[package] +name = "erc20" +version = "0.1.0" + +[dependencies] +starknet = ">=2.3.0-rc0" + +[[target.starknet-contract]] diff --git a/listings/ch01-applications/erc20/src/lib.cairo b/listings/ch01-applications/erc20/src/lib.cairo new file mode 100644 index 00000000..4ab2be17 --- /dev/null +++ b/listings/ch01-applications/erc20/src/lib.cairo @@ -0,0 +1,4 @@ +mod token; + +#[cfg(test)] +mod tests; diff --git a/listings/ch01-applications/erc20/src/tests.cairo b/listings/ch01-applications/erc20/src/tests.cairo new file mode 100644 index 00000000..361dba07 --- /dev/null +++ b/listings/ch01-applications/erc20/src/tests.cairo @@ -0,0 +1,2 @@ +mod tests { // TODO +} diff --git a/listings/ch01-applications/erc20/src/token.cairo b/listings/ch01-applications/erc20/src/token.cairo new file mode 100644 index 00000000..13301a37 --- /dev/null +++ b/listings/ch01-applications/erc20/src/token.cairo @@ -0,0 +1,214 @@ +use starknet::ContractAddress; + +// ANCHOR: interface +#[starknet::interface] +trait IERC20 { + 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::, + 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 { + 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 + diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 83ca39dc..d45643c6 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -25,6 +25,7 @@ Summary # Applications examples - [Upgradeable Contract](./ch01/upgradeable_contract.md) - [Defi Vault](./ch01/simple_vault.md) + - [ERC20 Token](./ch01/erc20.md) # Advanced concepts diff --git a/src/ch01/erc20.md b/src/ch01/erc20.md new file mode 100644 index 00000000..22dd1206 --- /dev/null +++ b/src/ch01/erc20.md @@ -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. From 54a16388909e724fedabb9570ba9f05a5d9dd5ef Mon Sep 17 00:00:00 2001 From: julio4 Date: Fri, 20 Oct 2023 15:18:43 +0900 Subject: [PATCH 2/2] fix: broken cairo format check quickfix --- listings/ch01-applications/erc20/src/token.cairo | 1 + 1 file changed, 1 insertion(+) diff --git a/listings/ch01-applications/erc20/src/token.cairo b/listings/ch01-applications/erc20/src/token.cairo index 13301a37..eb04fcfb 100644 --- a/listings/ch01-applications/erc20/src/token.cairo +++ b/listings/ch01-applications/erc20/src/token.cairo @@ -212,3 +212,4 @@ mod erc20 { } // ANCHOR_END: erc20 +