# Starknet Development Tutorial
## 1. Cairo Programs vs Starknet Contracts
### Overview
**Cairo Programs**: General-purpose programs written in Cairo for computation, not necessarily tied to blockchain. They run on the Starknet Virtual Machine (SVM) and can be used for off-chain computations or proofs.
**Starknet Contracts**: Cairo programs designed to run on Starknet, a Layer 2 blockchain. They include blockchain-specific features like storage, events, and syscalls, and are deployed to the Starknet network.
### Key Differences
| Feature | Cairo Program | Starknet Contract |
|---------|---------------|-------------------|
| Purpose | General computation | Blockchain logic |
| Storage | No persistent storage | Persistent storage variables |
| Execution Context | Off-chain (e.g., prover) | On-chain (Starknet network) |
| Syscalls | Limited or none | Access to blockchain syscalls |
| Deployment | Not deployed to blockchain | Deployed as a contract |
### Tutorial Steps
**1. Write a Cairo Program**: Create a simple program to compute a Fibonacci number (off-chain).
```cairo
fn fibonacci(n: u32) -> u32 {
if n <= 1 {
return n;
}
fibonacci(n - 1) + fibonacci(n - 2)
}
fn main() -> u32 {
fibonacci(10)
}
```
Save as `fibonacci.cairo`.
Compile and run using `cairo-run fibonacci.cairo` (requires Cairo CLI installed).
**2. Convert to a Starknet Contract**: Modify the program to store the result on-chain.
```cairo
#[starknet::contract]
mod FibonacciContract {
use core::starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
fib_result: u32,
}
#[external(v0)]
fn compute_and_store(ref self: ContractState, n: u32) {
let result = fibonacci(n);
self.fib_result.write(result);
}
fn fibonacci(n: u32) -> u32 {
if n <= 1 {
return n;
}
fibonacci(n - 1) + fibonacci(n - 2)
}
#[external(v0)]
fn get_result(self: @ContractState) -> u32 {
self.fib_result.read()
}
}
```
This contract stores the Fibonacci result in persistent storage.
Key additions: `#[starknet::contract]`, Storage struct, and ContractState.
**Key Takeaway**: Cairo programs are for general computation, while Starknet contracts extend Cairo with blockchain-specific features like storage and external functions.
## 2. Functions and Interfaces in Starknet
### Overview
**Functions**: Starknet contracts use functions to define logic. They can be:
- External: Callable from outside the contract (e.g., by users or other contracts).
- View: Read-only functions that don't modify storage.
- Internal: Private functions for internal logic.
**Interfaces**: Define a contract's external API, allowing other contracts to call its functions. In Starknet, interfaces are declared using `#[starknet::interface]`.
### Tutorial Steps
**1. Define a Contract with Functions**: Create a contract with different function types.
```cairo
#[starknet::contract]
mod Counter {
use core::starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
count: u64,
}
// External function to increment count
#[external(v0)]
fn increment(ref self: ContractState) {
let current = self.count.read();
self.count.write(current + 1);
}
// View function to read count
#[external(v0)]
fn get_count(self: @ContractState) -> u64 {
self.count.read()
}
// Internal function (not callable externally)
fn is_positive(self: @ContractState) -> bool {
self.count.read() > 0
}
}
```
**2. Create an Interface**: Define an interface for the Counter contract.
```cairo
#[starknet::interface]
trait ICounter<TContractState> {
fn increment(ref self: TContractState);
fn get_count(self: @TContractState) -> u64;
}
```
Save as `ICounter.cairo`.
The interface lists only the external functions that other contracts can call.
**3. Test the Interface**: Use a tool like Starknet Foundry to deploy and interact with the contract.
Install Starknet Foundry: `curl -L https://raw.githubusercontent.com/foundry-rs/starknet-foundry/master/scripts/install.sh | sh`.
Create a test file `tests/counter_test.cairo`:
```cairo
#[test]
fn test_counter() {
let contract = deploy_counter(); // Assume deployment logic
let dispatcher = ICounterDispatcher { contract_address: contract };
dispatcher.increment();
assert(dispatcher.get_count() == 1, 'Count should be 1');
}
```
Run tests with `snforge test`.
**Key Takeaway**: Functions define contract logic, and interfaces ensure interoperability by exposing external functions to other contracts.
## 3. Writing and Deploying a Simple Storage Contract
### Overview
A storage contract stores data persistently on Starknet. This section guides you through writing, compiling, and deploying a simple storage contract.
### Tutorial Steps
**1. Write the Contract**: Create a contract to store and retrieve a single value.
```cairo
#[starknet::contract]
mod SimpleStorage {
use core::starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
value: u256,
}
#[constructor]
fn constructor(ref self: ContractState, initial_value: u256) {
self.value.write(initial_value);
}
#[external(v0)]
fn set_value(ref self: ContractState, new_value: u256) {
self.value.write(new_value);
}
#[external(v0)]
fn get_value(self: @ContractState) -> u256 {
self.value.read()
}
}
```
Save as `simple_storage.cairo`.
The constructor initializes the storage variable, and set_value/get_value modify/read it.
**2. Compile the Contract**: Use the Cairo compiler.
Install Cairo: Follow instructions at https://github.com/starkware-libs/cairo.
Compile:
```bash
scarb build
```
This generates a JSON file with the contract's Sierra code.
**3. Deploy to Starknet Testnet**: Use Starkli for deployment.
Install Starkli: `curl https://get.starkli.sh | sh`.
Set up a Starknet account:
```bash
starkli account oz init ~/.starkli/my_account.json
starkli account oz deploy ~/.starkli/my_account.json
```
Declare the contract:
```bash
starkli declare simple_storage_compiled.json --account ~/.starkli/my_account.json --rpc https://starknet-sepolia.infura.io/v3/<YOUR_INFURA_KEY>
```
Deploy the contract:
```bash
starkli deploy <CLASS_HASH> 100 --account ~/.starkli/my_account.json --rpc https://starknet-sepolia.infura.io/v3/<YOUR_INFURA_KEY>
```
100 is the initial_value for the constructor.
Note the contract address returned.
**4. Interact with the Contract**: Use Starkli to call functions.
Read the value:
```bash
starkli call <CONTRACT_ADDRESS> get_value --rpc https://starknet-sepolia.infura.io/v3/<YOUR_INFURA_KEY>
```
Set a new value:
```bash
starkli invoke <CONTRACT_ADDRESS> set_value 200 --account ~/.starkli/my_account.json --rpc https://starknet-sepolia.infura.io/v3/<YOUR_INFURA_KEY>
```
**Key Takeaway**: Writing a storage contract involves defining storage variables and external functions. Deployment requires compiling to Sierra code and using tools like Starkli.
## 4. Understanding Events, Traits, and Syscalls
### Overview
**Events**: Mechanisms to log data on-chain, useful for tracking state changes.
**Traits**: Define shared behavior (like interfaces in other languages) for functions.
**Syscalls**: Low-level calls to interact with Starknet's runtime (e.g., reading caller address).
### Tutorial Steps
**1. Define a Contract with Events**: Add an event to the storage contract.
```cairo
#[starknet::contract]
mod StorageWithEvents {
use core::starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
use starknet::event::EventEmitter;
#[storage]
struct Storage {
value: u256,
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
ValueChanged: ValueChanged,
}
#[derive(Drop, starknet::Event)]
struct ValueChanged {
old_value: u256,
new_value: u256,
}
#[external(v0)]
fn set_value(ref self: ContractState, new_value: u256) {
let old_value = self.value.read();
self.value.write(new_value);
self.emit(ValueChanged { old_value, new_value });
}
#[external(v0)]
fn get_value(self: @ContractState) -> u256 {
self.value.read()
}
}
```
The ValueChanged event logs the old and new values when set_value is called.
**2. Use a Trait**: Define a trait for storage operations.
```cairo
trait StorageTrait<TContractState> {
fn set_value(ref self: TContractState, new_value: u256);
fn get_value(self: @TContractState) -> u256;
}
```
Ensure the contract implements the trait by matching function signatures.
**3. Incorporate a Syscall**: Use get_caller_address to restrict set_value.
```cairo
use starknet::syscalls::get_caller_address;
#[external(v0)]
fn set_value(ref self: ContractState, new_value: u256) {
let caller = get_caller_address();
assert(caller != 0, 'Invalid caller'); // Prevent zero address
let old_value = self.value.read();
self.value.write(new_value);
self.emit(ValueChanged { old_value, new_value });
}
```
get_caller_address retrieves the address of the caller.
**4. Deploy and Test**: Follow the deployment steps from Section 3. Use a block explorer like Voyager to verify emitted events.
Call set_value and check the transaction on https://sepolia.voyager.online/.
Look for the ValueChanged event in the transaction details.
**Key Takeaway**: Events log state changes, traits define reusable function signatures, and syscalls enable interaction with Starknet's runtime.
## 5. Cross-Contract Interactions and Libraries
### Overview
**Cross-Contract Interactions**: One contract calls functions on another using an interface and dispatcher.
**Libraries**: Reusable Cairo code that doesn't store state, imported as modules.
### Tutorial Steps
**1. Create a Second Contract**: Write a contract that interacts with the Counter contract (from Section 2).
```cairo
#[starknet::contract]
mod CounterCaller {
use starknet::contract_address::ContractAddress;
#[starknet::interface]
trait ICounter<TContractState> {
fn increment(ref self: TContractState);
fn get_count(self: @TContractState) -> u64;
}
#[storage]
struct Storage {
counter_address: ContractAddress,
}
#[constructor]
fn constructor(ref self: ContractState, counter_address: ContractAddress) {
self.counter_address.write(counter_address);
}
#[external(v0)]
fn call_counter(ref self: ContractState) {
let counter = ICounterDispatcher { contract_address: self.counter_address.read() };
counter.increment();
}
#[external(v0)]
fn get_counter_value(self: @ContractState) -> u64 {
let counter = ICounterDispatcher { contract_address: self.counter_address.read() };
counter.get_count()
}
}
```
This contract calls increment on the Counter contract and reads its value.
**2. Write a Library**: Create a reusable library for math operations.
```cairo
mod MathLib {
fn add(a: u256, b: u256) -> u256 {
a + b
}
fn multiply(a: u256, b: u256) -> u256 {
a * b
}
}
```
Save as `math_lib.cairo`.
**3. Use the Library in a Contract**: Import MathLib into SimpleStorage.
```cairo
#[starknet::contract]
mod SimpleStorage {
use super::MathLib;
use core::starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
value: u256,
}
#[external(v0)]
fn set_value(ref self: ContractState, a: u256, b: u256) {
self.value.write(MathLib::add(a, b));
}
#[external(v0)]
fn get_value(self: @ContractState) -> u256 {
self.value.read()
}
}
```
**4. Deploy and Test Cross-Contract Interaction**:
Deploy the Counter contract and note its address.
Deploy CounterCaller, passing the Counter address to the constructor.
Call call_counter on CounterCaller and verify that Counter's count increases using get_counter_value.
**Key Takeaway**: Cross-contract interactions use interfaces and dispatchers to call external functions. Libraries provide reusable, state-less code.
## 6. Modules and Scope in Starknet
### Overview
**Modules**: Organize code into reusable units, similar to namespaces.
**Scope**: Defines visibility and access to functions and variables (e.g., pub for public access).
### Tutorial Steps
**1. Create a Modular Contract**: Split the SimpleStorage contract into modules.
```cairo
mod storage {
use core::starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
pub struct Storage {
pub value: u256,
}
pub fn set_value(ref self: ContractState, new_value: u256) {
self.value.write(new_value);
}
pub fn get_value(self: @ContractState) -> u256 {
self.value.read()
}
}
#[starknet::contract]
mod SimpleStorage {
use super::storage;
#[storage]
struct Storage {
inner: storage::Storage,
}
#[external(v0)]
fn set(ref self: ContractState, new_value: u256) {
storage::set_value(ref self.inner, new_value);
}
#[external(v0)]
fn get(self: @ContractState) -> u256 {
storage::get_value(@self.inner)
}
}
```
The storage module encapsulates storage logic.
Use pub to make functions and storage accessible.
**2. Test Scope Rules**: Attempt to access a private function.
```cairo
mod utils {
fn private_helper() -> u256 {
42
}
pub fn public_helper() -> u256 {
private_helper()
}
}
#[starknet::contract]
mod TestScope {
use super::utils;
#[storage]
struct Storage {}
#[external(v0)]
fn try_access(self: @ContractState) -> u256 {
utils::public_helper() // Works
// utils::private_helper() // Fails: private_helper is not visible
}
}
```
Compile to verify that private_helper is inaccessible outside its module.
**3. Organize a Project**: Structure a Starknet project with multiple modules.
Directory structure:
```
my_project/
├── src/
│ ├── storage.cairo
│ ├── utils.cairo
│ ├── main.cairo
│ └── lib.cairo
```
lib.cairo:
```cairo
mod storage;
mod utils;
mod main;
```
Use Starknet Foundry to compile and test the project:
```bash
snforge init my_project
snforge build
snforge test
```
**Key Takeaway**: Modules organize code for reusability and clarity. Scope controls access, with pub exposing functions and data to other modules or contracts.
## Prerequisites and Tools
**Cairo**: Install from https://github.com/starkware-libs/cairo.
**Starknet Foundry**: For testing (https://foundry-rs.github.io/starknet-foundry/).
**Starkli**: For deployment (https://github.com/xJonathanLEI/starkli).
**Testnet**: Use Goerli testnet with an Infura key or a Starknet node.
**IDE**: Use VSCode with the Cairo extension for syntax highlighting.