---
# System prepended metadata

title: Starknet Development Tutorial

---

# 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.