Note! This tutorial is outdated due to changes in the Cairo language. Please refer to this article for the latest version 1.10 and syntax: https://starknetastro.xlog.app/Starknet_Shanghai_Workshop_DAY1#
Introduction#
This course serves as a quick-start BootCamp and will not delve too deeply into the features of the Cairo language (but our last class will provide a slightly deeper introduction to other useful parts of the Cairo language).
If you want to study Cairo in depth beforehand, you can check out our recently translated Cairo-Book Chinese version.
Environment Configuration#
Minimum installation options:
This course requires that you have installed Cairo 1.0, as well as all the necessary dependencies for the Starknet Cairo 0.x CLI, which we required in the previous class.
Note! Due to the upgrade of DevNet, only starknet-compile 1.1.0
is now supported, so you need to upgrade Cairo 1.0 to v1.1.0.
Execute the following commands in the directory where you installed Cairo:
git checkout main
git pull
cargo build --all --release
Additionally, based on feedback from the last class, many students were still unable to compile properly after installing Python, so here are some common solutions:
linker 'cc' not found
, the cc linker cannot be found, which can be resolved with the following solution.
sudo apt install build-essential
Of course, to be safe, you can install a complete set:
sudo apt-get install build-essential libncursesw5-dev libgdbm-dev libc6-dev zlib1g-dev libsqlite3-dev tk-dev libssl-dev openssl libbz2-dev libreadline-dev
ModuleNotFoundError: No module named '_ctypes'
The solution is a bit troublesome; you need to reinstall Python.
pyenv uninstall 3.9.16
sudo yum install libffi-devel
pyenv install 3.9.16
use_2to3 is invalid.
Sometimes this occurs inexplicably.
pip install -U setuptools
Install CLI#
To facilitate operations, let's first enter the root directory of Cairo 1.0 (refer to the directory where you installed 1.0 in the last class, for example, mine is as follows)
cd ~/cairo/
Create a test folder:
mkdir -p starknetastro/camp1_lesson2/
cd starknetastro/camp1_lesson2/
Next, create a Python virtual environment:
python3.9 -m venv venv
Activate the virtual environment:
source venv/bin/activate
At this point, you should see a (venv) prefix in your terminal. Let's install the CLI
(venv) camp1 $ pip install cairo-lang
Check if the installation was successful
(venv) camp1 $ starknet --version
It should output:
starknet 0.11.2
Configure Starknet Testnet Account#
Next, let's configure the testnet account. First, define a few environment variables as follows:
# Specify the testnet
export STARKNET_NETWORK=alpha-goerli
# Set the default wallet implementation
export STARKNET_WALLET=starkware.starknet.wallets.open_zeppelin.OpenZeppelinAccount
# Specify the compiler path as the path for Cairo 1 (please modify according to your own path)
export CAIRO_COMPILER_DIR=~/cairo/target/release/
You can then save this in your .zshrc
or .bashrc
so that you can still use the environment variables after closing the terminal. The method will not be elaborated here.
We use the following two commands to test
(venv) $ $CAIRO_COMPILER_DIR/cairo-compile --version
cairo-compile 1.1.0
(venv) $ $CAIRO_COMPILER_DIR/starknet-compile --version
starknet-compile 1.1.0
Both should output 1.1.0, indicating that the environment is configured correctly.
Create a Testnet Wallet#
Next, let's create a wallet on the testnet:
starknet new_account --account StarknetAstro
This should output
Account address: 0xYourAddress
Public key: 0xYourPublicKey
Move the appropriate amount of funds to the account, and then deploy the account by invoking the 'starknet deploy_account' command.
NOTE: This is a modified version of the OpenZeppelin account contract. The signature is computed differently.
Next, we need to deploy it on the testnet. Deploying in Starknet is also a transaction, so gas is required. We can send some test ETH to 0xYourAddress
via a cross-chain bridge or the official Faucet.
Then use this command to deploy
starknet deploy_account --account StarknetAstro
Output:
Sending the transaction with max_fee: 0.000114 ETH (113796902644445 WEI).
Sent deploy account contract transaction.
Contract address: 0xYourAddress
Transaction hash: 0xTransactionAddress
Deployment Test#
Let's first try to deploy a test contract to see if it can be deployed normally.
Execute the following commands:
mkdir src sierra
touch src/example.cairo
Then, fill in the test content in src/example.cairo
:
#[contract]
mod SimpleStorage {
#[starknet::storage]
struct Storage {
}
}
Next, we will compile the Cairo file into a Sierra file:
$CAIRO_COMPILER_DIR/starknet-compile src/example.cairo sierra/example.json
After successful compilation, we need to declare the contract's ClassHash:
starknet declare --contract sierra/example.json --account StarknetAstro
Of course, since the same ClassHash cannot be declared multiple times, this test contract has already been declared, so you will get an error output indicating that it has already been declared, which is not a problem:
Got BadRequest while trying to access https://alpha4.starknet.io/feeder_gateway/simulate_transaction?blockNumber=pending&skipValidate=false. Status code: 500;
text: {"code": "StarknetErrorCode.CLASS_ALREADY_DECLARED",
"message": "Class with hash 0x695874cd8feed014ebe379df39aa0dcef861ff495cc5465e84927377fa8e7e6 is already declared.
0x317d3ac2cf840e487b6d0014a75f0cf507dff0bc143c710388e323487089bfa != 0”}.
Next, let's actually deploy the contract instance:
starknet deploy --class_hash 0x695874cd8feed014ebe379df39aa0dcef861ff495cc5465e84927377fa8e7e6 --account StarknetAstro
Output:
Sending the transaction with max_fee: 0.000132 ETH (132082306595047 WEI).
Invoke transaction for contract deployment was sent.
Contract address: 0x060e17c12d4e3fee8af2e28e6a310a3192a1b1190d060cb4324e234213d72b64
Transaction hash: 0x73f995d1fd9a05161e271f5ffb879bd70b79185a2b3de5536a289780387dd30
This indicates that the actual deployment was successful! You can see our deployed contract here.
ERC20 Code Template#
Save the following erc20.cairo in the src
created above for easy terminal operations.
File name: erc20.cairo
use starknet::ContractAddress;
#[starknet::interface]
trait IERC20<TStorage> {
fn name(self: @TStorage) -> felt252;
fn symbol(self: @TStorage) -> felt252;
fn decimals(self: @TStorage) -> u8;
fn totalSupply(self: @TStorage) -> u256;
fn balanceOf(self: @TStorage, account: ContractAddress) -> u256;
fn allowance(self: @TStorage, owner: ContractAddress, spender: ContractAddress) -> u256;
fn transfer(ref self: TStorage, recipient: ContractAddress, amount: u256);
fn transferFrom(
ref self: TStorage, sender: ContractAddress, recipient: ContractAddress, amount: u256
);
fn approve(ref self: TStorage, spender: ContractAddress, amount: u256);
fn increase_allowance(ref self: TStorage, spender: ContractAddress, added_value: u256);
fn decrease_allowance(ref self: TStorage, spender: ContractAddress, subtracted_value: u256);
}
#[contract]
mod ERC20 {
use zeroable::Zeroable;
use starknet::get_caller_address;
use starknet::contract_address_const;
use starknet::ContractAddress;
#[starknet::storage]
struct Storage {
name: felt252,
symbol: felt252,
decimals: u8,
totalSupply: u256,
balances: LegacyMap::<ContractAddress, u256>,
allowances: LegacyMap::<(ContractAddress, ContractAddress), u256>,
}
#[event]
fn Transfer(from: ContractAddress, to: ContractAddress, value: u256) {}
#[event]
fn Approval(owner: ContractAddress, spender: ContractAddress, value: u256) {}
#[constructor]
fn constructor(
ref self: Storage,
name: felt252,
symbol: felt252,
decimals: u8,
initialSupply: u256,
recipient: ContractAddress
) {
self.name.write(name);
self.symbol.write(symbol);
self.decimals.write(decimals);
assert(!recipient.is_zero(), 'ERC20: mint to the 0 address');
self.totalSupply.write(initialSupply);
self.balances.write(recipient, initialSupply);
}
#[external]
impl IERC20Impl of super::IERC20<Storage> {
fn name(self: @Storage) -> felt252 {
self.name.read()
}
fn symbol(self: @Storage) -> felt252 {
self.symbol.read()
}
fn decimals(self: @Storage) -> u8 {
self.decimals.read()
}
fn totalSupply(self: @Storage) -> u256 {
self.totalSupply.read()
}
fn balanceOf(self: @Storage, account: ContractAddress) -> u256 {
self.balances.read(account)
}
fn allowance(self: @Storage, owner: ContractAddress, spender: ContractAddress) -> u256 {
self.allowances.read((owner, spender))
}
fn transfer(ref self: Storage, recipient: ContractAddress, amount: u256) {
let sender = get_caller_address();
self.transfer_helper(sender, recipient, amount);
}
fn transferFrom(
ref self: Storage, sender: ContractAddress, recipient: ContractAddress, amount: u256
) {
let caller = get_caller_address();
self.transfer_helper(sender, recipient, amount);
}
fn approve(ref self: Storage, spender: ContractAddress, amount: u256) {
let caller = get_caller_address();
self.approve_helper(caller, spender, amount);
}
fn increase_allowance(ref self: Storage, 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: Storage, spender: ContractAddress, subtracted_value: u256) {
let caller = get_caller_address();
self
.approve_helper(
caller, spender, self.allowances.read((caller, spender)) - subtracted_value
);
}
}
#[generate_trait]
impl StorageImpl of StorageTrait {
fn transfer_helper(
ref self: Storage, sender: ContractAddress, recipient: ContractAddress, amount: u256
) {
assert(!sender.is_zero(), 'ERC20: transfer from 0');
assert(!recipient.is_zero(), 'ERC20: transfer to 0');
self.balances.write(sender, self.balances.read(sender) - amount);
self.balances.write(recipient, self.balances.read(recipient) + amount);
Transfer(sender, recipient, amount);
}
fn approve_helper(
ref self: Storage, owner: ContractAddress, spender: ContractAddress, amount: u256
) {
assert(!spender.is_zero(), 'ERC20: approve from 0');
self.allowances.write((owner, spender), amount);
Approval(owner, spender, amount);
}
}
}
Deploy ERC20#
First, we still need to compile the Cairo file to Sierra
$CAIRO_COMPILER_DIR/starknet-compile src/erc20.cairo sierra/erc20.json
Next is the declaration
starknet declare --contract sierra/erc20.json --account StarknetAstro
This time the output is different because the erc20 file we wrote has not been deployed by anyone, so there will be no error (of course, students following the course will still encounter the already declared error).
Sending the transaction with max_fee: 0.000101 ETH (101461098713976 WEI).
Declare transaction was sent.
Contract class hash: 0x5d144ad5b9af2752da0ef741b565d334a1cb18d02224462d978ebf0a6e8e919
Transaction hash: 0x21da071ce36db1544f482150a8b99902ca1b5db090b66618de724362408ea00
Next, deploy the instance of this erc20 contract. Note that the constructor in the erc20 code has parameters, so we also need to provide the corresponding parameters:
starknet deploy --inputs 0x417374726F546F6B656E 0x417374726F 0x12 0x3e8 0x0 0x04202409943437c6ebe83697e1ba183976912b667066db7b8796064e59426b0e --class_hash 0x5d144ad5b9af2752da0ef741b565d334a1cb18d02224462d978ebf0a6e8e919 --account StarknetAstro
Output:
Sending the transaction with max_fee: 0.000791 ETH (790973560660141 WEI).
Invoke transaction for contract deployment was sent.
Contract address: 0x059128b7b3001106211376c12ee86feedd9247033cd1a771e8957efac3b61ca8
Transaction hash: 0x4bcf2d5f9a2e2090755d7a07d2849d4e5cf13fbdd36befd4f73d2030cea392c
Great job! You can see our deployed contract here
).