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