--- language: Cairo filename: learnCairo.sol contributors: - ["Darlington Nnam", "https://github.com/Darlington02"] --- # Cairo Cairo is a Turing-complete language that allows you write provable programs (where one party can prove to another that a certain computation was executed correctly) on StarkNet. # StarkNet StarkNet is a decentralized ZK-rollup that operates as an Ethereum layer 2 chain. In this document, we are going to be going in-depth into understanding Cairo's syntax and how you could create and deploy a Cairo smart contract on StarkNet. **NB: As at the time of this writing, StarkNet is still at v0.10.3, with Cairo 1.0 coming soon. The ecosystem is young and evolving very fast, so you might want to check the [official docs](https://www.cairo-lang.org/docs) to confirm this document is still up-to-date. Pull requests are welcome!** # Setting Up A Development Environment Before we get started writing codes, we will need to setup a Cairo development environment, for writing, compiling and deploying our contracts to StarkNet. For the purpose of this tutorial we are going to be using the [Protostar Framework](https://github.com/software-mansion/protostar). Installation steps can be found in the docs [here](https://docs.swmansion.com/protostar/docs/tutorials/installation). Note that Protostar supports just Mac and Linux OS, Windows users might need to use WSL, or go for other alternatives such as the Official [StarkNet CLI](https://www.cairo-lang.org/docs/quickstart.html) or [Nile from Openzeppelin](https://github.com/OpenZeppelin/nile) Once you're done with the installations, run the command `protostar -v` to confirm your installation was successful. If successful, you should see your Protostar version displayed on the screen. ## Initializing a new project Protostar similar to Truffle for solidity development can be installed once and used for multiple projects. To initialize a new Protostar project, run the following command: ``` protostar init ``` 2. It would then request the project's name and the library's directory name, you'd need to fill in this, and a new project will be initialized successfully. # Compiling, Declaring, Deploying And Interacting With StarkNet Contracts Within the `src` folder you'll find a boilerplate contract that comes with initializing a new Protostar project, `main.cairo`. We are going to be compiling, declaring and deploying this contract. ## Compiling Contracts To compile a Cairo contract using Protostar, ensure a path to the contract is specified in the `[contracts]` section of the `protostar.toml` file. Once you've done that, open your terminal and run the command: ``` protostar build ``` And you should get an output similar to what you see below, with a `main.json` and `main_abi.json` files created in the `build` folder. building your contract ## Declaring Contracts With the recent StarkNet update to 0.10.3, the DEPLOY transaction was deprecated and no longer works. To deploy a transaction, you must first declare a Contract to obtain the class hash, then deploy the declared contract using the [Universal Deployer Contract](https://community.starknet.io/t/universal-deployer-contract-proposal/1864). Before declaring or deploying your contract using Protostar, you should set the private key associated with the specified account address in a file, or in the terminal. To set your private key in the terminal, run the command: ``` export PROTOSTAR_ACCOUNT_PRIVATE_KEY=[YOUR PRIVATE KEY HERE] ``` Then to declare our contract using Protostar run the following command: ``` protostar declare ./build/main.json --network testnet --account 0x0691622bBFD29e835bA4004e7425A4e9630840EbD11c5269DE51C16774585b16 --max-fee auto ``` where `network` specifies the network we are deploying to, `account` specifies account whose private key we are using, `max-fee` specifies the maximum fee to be paid for the transaction. You should get the class hash outputted as seen below: declaring your contract ## Deploying Contracts After obtaining our class hash from declaring, we can now deploy using the below command: ``` protostar deploy 0x02a5de1b145e18dfeb31c7cd7ff403714ededf5f3fdf75f8b0ac96f2017541bc --network testnet --account 0x0691622bBFD29e835bA4004e7425A4e9630840EbD11c5269DE51C16774585b16 --max-fee auto ``` where `0x02a5de1b145e18dfeb31c7cd7ff403714ededf5f3fdf75f8b0ac96f2017541bc` is the class hash of our contract. deploying your contract ## Interacting With Contracts To interact with your deployed contract, we will be using Argent X (alternative - Braavos), and Starkscan (alternative - Voyager). To install and setup Argent X, check out this [guide](https://www.argent.xyz/learn/how-to-create-an-argent-x-wallet/). Copy your contract address, displayed on screen from the previous step, and head over to [Starkscan](https://testnet.starkscan.co/) to search for the contract. Once found, you can make write calls to the contract by following the steps below: 1. Click on the "connect wallet" button connect wallet 2. Select Argent X and approve the connection connect to argentX 3. You can now make read and write calls easily. # Let's learn Cairo First let's look at a default contract that comes with Protostar ```cairo // Allows you to set balance on deployment, increase, and get the balance. // Language directive - instructs compiler its a StarkNet contract %lang starknet // Library imports from the Cairo-lang library from starkware.cairo.common.math import assert_nn from starkware.cairo.common.cairo_builtins import HashBuiltin // @dev Storage variable that stores the balance of a user. // @storage_var is a decorator that instructs the compiler the function // below it is a storage variable. @storage_var func balance() -> (res: felt) { } // @dev Constructor writes the balance variable to 0 on deployment // Constructors sets storage variables on deployment. Can accept arguments too. @constructor func constructor{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() {balance.write(0); return (); } // @dev increase_balance updates the balance variable // @param amount the amount you want to add to balance // @external is a decorator that specifies the func below it is an external // function. @external func increase_balance{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(amount: felt){ with_attr error_message("Amount must be positive. Got: {amount}.") { assert_nn(amount); } let (res) = balance.read(); balance.write(res + amount); return (); } // @dev returns the balance variable // @view is a decorator that specifies the func below it is a view function. @view func get_balance{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> (res: felt) { let (res) = balance.read(); return (res,); } // before proceeding, try to build, deploy and interact with this contract! // NB: Should be at main.cairo if you are using Protostar. ``` Now unto the main lessons ### 1. THE FELT DATA TYPE ```cairo // Unlike solidity, where you have access to various data types, Cairo // comes with just a single data type..felts // Felts stands for Field elements, and are a 252 bit integer in the range // 0<=x<=P where P is a prime number. // You can create a Uint256 in Cairo by utlizing a struct of two 128 bits // felts. struct Uint256 { low: felt, // The low 128 bits of the value. high: felt, // The high 128 bits of the value. } // To avoid running into issues with divisions, it's safer to work with the // unsigned_div_rem method from Cairo-lang's library. ``` ### 2. LANG DIRECTIVE AND IMPORTS ```cairo // To get started with writing a StarkNet contract, you must specify the // directive: %lang starknet // This directive informs the compiler you are writing a contract and not a // program. // The difference between both is contracts have access to StarkNet's // storage, programs don't and as such are stateless. // There are important functions you might need to import from the official // Cairo-lang library or Openzeppelin's. e.g. from starkware.cairo.common.cairo_builtins import HashBuiltin from cairo_contracts.src.openzeppelin.token.erc20.library import ERC20 from starkware.cairo.common.uint256 import Uint256 from starkware.cairo.common.bool import TRUE ``` ### 3. DATA STRUCTURES ```cairo // A. STORAGE VARIABLES // Cairo's storage is a map with 2^251 slots, where each slot is a felt // which is initialized to 0. // You create one using the @storage_var decorator @storage_var func names() -> (name: felt){ } // B. STORAGE MAPPINGS // Unlike soldity where mappings have a separate keyword, in Cairo you // create mappings using storage variables. @storage_var func names(address: felt) -> (name: felt){ } // C. STRUCTS // Structs are a means to create custom data types in Cairo. // A Struct has a size, which is the sum of the sizes of its members. The // size can be retrieved using MyStruct.SIZE. // You create a struct in Cairo using the `struct` keyword. struct Person { name: felt, age: felt, address: felt, } // D. CONSTANTS // Constants are fixed and as such can't be altered after being set. // They evaluate to an integer (field element) at compile time. // To create a constant in Cairo, you use the `const` keyword. // Its proper practice to capitalize constant names. const USER = 0x01C6cfC1DB2ae90dACEA243F0a8C2F4e32560F7cDD398e4dA2Cc56B733774E9b // E. ARRAYS // Arrays can be defined as a pointer(felt*) to the first element of the //array. // As an array is populated, its elements take up contigous memory cells. // The `alloc` keyword can be used to dynamically allocate a new memory // segment, which can be used to store an array let (myArray: felt*) = alloc (); assert myArray[0] = 1; assert myArray[1] = 2; assert myArray[3] = 3; // You can also use the `new` operator to create fixed-size arrays using //tuples // The new operator is useful as it enables you allocate memory and // initialize the object in one instruction func foo() { tempvar arr: felt* = new (1, 1, 2, 3, 5); assert arr[4] = 5; return (); } // F. TUPLES // A tuple is a finite, ordered, unchangeable list of elements // It is represented as a comma-separated list of elements enclosed by // parentheses // Their elements may be of any combination of valid types. local tuple0: (felt, felt, felt) = (7, 9, 13); // G. EVENTS // Events allows a contract emit information during the course of its // execution, that can be used outside of StarkNet. // To create an event: @event func name_stored(address, name) { } // To emit an event: name_stored.emit(address, name); ``` ### 4. CONSTRUCTORS, EXTERNAL AND VIEW FUNCTIONS ```cairo // A. CONSTRUCTORS // Constructors are a way to intialize state variables on contract // deployment // You create a constructor using the @constructor decorator @constructor func constructor{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(_name: felt) { let (caller) = get_caller_address(); names.write(caller, _name); return (); } // B. EXTERNAL FUNCTIONS // External functions are functions that modifies the state of the network // You create an external function using the @external decorator @external func store_name{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(_name: felt){ let (caller) = get_caller_address(); names.write(caller, _name); stored_name.emit(caller, _name); return (); } // C. VIEW FUNCTIONS // View functions do not modify the state of the blockchain // You can create a view function using the @view decorator @view func get_name{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(_address: felt) -> (name: felt){ let (name) = names.read(_address); return (name,); } // NB: Unlike Solidity, Cairo supports just External and View function // types. // You can alternatively also create an internal function by not adding any // decorator to the function. ``` ### 5. DECORATORS ```cairo // All functions in Cairo are specified by the `func` keyword, which can be // confusing. // Decorators are used by the compiler to distinguish between these // functions. // Here are the most common decorators you'll encounter in Cairo: // 1. @storage_var — used for specifying state variables. // 2. @constructor — used for specifying constructors. // 3. @external — used for specifying functions that write to a state // variable. // 4. @event — used for specifying events // 5. @view — used for specifying functions that reads from a state // variable. // 6. @contract_interface - used for specifying function interfaces. // 7. @l1_handler — used for specifying functions that processes message // sent from an L1 contract in a messaging bridge. ``` ### 6. BUILTINS, HINTS & IMPLICIT ARGUMENTS ```cairo // A. BUILTINS // Builtins are predefined optimized low-level execution units, which are // added to Cairo’s CPU board. // They help perform predefined computations like pedersen hashing, bitwise // operations etc, which are expensive to perform in Vanilla Cairo. // Each builtin in Cairo, is assigned a separate memory location, // accessible through regular Cairo memory calls using implicit parameters. // You specify them using the %builtins directive // Here is a list of available builtins in Cairo: // 1. output — the output builtin is used for writing program outputs // 2. pedersen — the pedersen builtin is used for pedersen hashing // computations // 3. range_check — This builtin is mostly used for integer comparisons, // and facilitates check to confirm that a field element is within a range [0, // 2^128) // 4. ecdsa — the ecdsa builtin is used for verifying ECDSA signatures // 5. bitwise — the bitwise builtin is used for carrying out bitwise // operations on felts // B. HINTS // Hints are pieces of Python codes, which contains instructions that only // the prover sees and executes // From the point of view of the verifier these hints do not exist // To specify a hint in Cairo, you need to encapsulate it within %{ and%} // Its good practice to avoid using hints as much as you can in your // contracts, as hints are not added to the bytecode, and thus do not count in the // total number of execution steps. %{ # Python hint goes here %} // C. IMPLICIT ARGUMENTS // Implicit arguments are not restricted to the function body, but can be // inherited by other functions calls that require them. // Implicit arguments are passed in between curly bracelets, like you can // see below: func store_name{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(_name: felt){ let (caller) = get_caller_address(); names.write(caller, _name); stored_name.emit(caller, _name); return (); } ``` ### 7. ERROR MESSAGES & ACCESS CONTROLS ```cairo // You can create custom errors in Cairo which is outputted to the user // upon failed execution. // This can be very useful for implementing checks and proper access // control mechanisms. // An example is preventing a user to call a function except user is admin. // imports from starkware.starknet.common.syscalls import get_caller_address // create an admin constant const ADMIN = 0x01C6cfC1DB2ae90dACEA243F0a8C2F4e32560F7cDD398e4dA2Cc56B733774E9b // implement access control with_attr error_message("You do not have access to make this action!"){ let (caller) = get_caller_address(); assert ADMIN = caller; } // using an assert statement throws if condition is not true, thus // returning the specified error. ``` ### 8. CONTRACT INTERFACES ```cairo // Contract interfaces provide a means for one contract to invoke or call // the external function of another contract. // To create a contract interface, you use the @contract_interface keyword @contract_interface namespace IENS { func store_name(_name: felt) { } func get_name(_address: felt) -> (name: felt) { } } // Once a contract interface is specified, any contract can make calls to // that contract passing in the contract address as the first parameter like this: IENS.store_name(contract_address, _name); // Note that Interfaces excludes the function body/logic and the implicit // arguments. ``` ### 9. RECURSIONS ```cairo // Due to the unavailability of loops, Recursions are the go-to for similar // operations. // In simple terms, a recursive function is one which calls itself // repeatedly. // A good example to demonstrate this is writing a function for getting the // nth fibonacci number: @external func fibonacci{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(n : felt) -> (result : felt){ alloc_locals; if (n == 0){ return (0); } if (n == 1){ return (1); } let (local x) = fibonacci(n - 1); let (local y) = fibonacci(n - 2); return (result=(x + y)); } // The nth fibonacci term is the sum of the nth - 1 and the nth - 2 // numbers, that's why we get these two as (x, y) using recursion. // NB: when implementing recursive functions, always remember to implement // a base case (n==0, n==1 in our case), to prevent stack overflow. ``` Some low-level stuffs ### 10. REGISTERS ```cairo // Registers holds values that may change over time. // There are 3 major types of Registers: // 1. ap (allocation pointer) points to a yet unused memory. Temporary // variables created using `let`, `tempvar` are held here, and thus susceptible to // being revoked // 2. fp (frame pointer) points to the frame of the current function. The // address of all the function arguments and local variables are relative to this // register and as such can never be revoked // 3. pc (program counter) points to the current instruction ``` ### 11. REVOKED REFERENCES ```cairo // Revoked references occurs when there is a call instruction to another // function, between the definition of a reference variable that depends on // `ap`(temp variables) and its usage. This occurs as the compiler may not be able // to compute the change of `ap` (as one may jump to the label from another place // in the program, or call a function that might change ap in an unknown way). // Here is an example to demonstrate what I mean: @external func get_balance{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> (res: felt) { return (res=100); } @external func double_balance{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> (res: felt) { let multiplier = 2; let (balance) = get_balance(); let new_balance = balance * multiplier; return (res=new_balance); } // If you run that code, you'll run into the revoked reference error as we // are trying to access the `multiplier` variable after calling the get_balance // function; // To solve revoked references, In simple cases you can resolve this issue, // by adding the keyword, `alloc_locals` within function scopes, but in most // complex cases you might need to create a local variable to resolve it. // resolving the `double_balance` function: @external func double_balance{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> (res: felt) { alloc_locals; let multiplier = 2; let (balance) = get_balance(); let new_balance = balance * multiplier; return (res=new_balance); } ``` Miscellaneous ### 12. Understanding Cairo's punctuations ```cairo // ; (semicolon). Used at the end of each instruction // ( ) (parentheses). Used in a function declaration, if statements, and in // a tuple declaration // { } (curly brackets). Used in a declaration of implicit arguments and to // define code blocks. // [ ] (square brackets). Standalone brackets represent the value at a // particular address location (such as the allocation pointer, [ap]). Brackets // following a pointer or a tuple act as a subscript operator, where x[2] // represents the element with index 2 in x. // * Single asterisk. Refers to the pointer of an expression. // % Percent sign. Appears at the start of a directive, such as %builtins // or %lang. // %{ %} Represents Python hints. // _ (underscore). A placeholder to handle values that are not used, such // as an unused function return value. ``` # FULL CONTRACT EXAMPLE Below is a simple automated market maker contract example that implements most of what we just learnt! Re-write, deploy, have fun! ```cairo %lang starknet from starkware.cairo.common.cairo_builtins import HashBuiltin from starkware.cairo.common.hash import hash2 from starkware.cairo.common.alloc import alloc from starkware.cairo.common.math import (assert_le, assert_nn_le, unsigned_div_rem) from starkware.starknet.common.syscalls import (get_caller_address, storage_read, storage_write) // // CONSTANTS // // @dev the maximum amount of each token that belongs to the AMM const BALANCE_UPPER_BOUND = 2 ** 64; const TOKEN_TYPE_A = 1; const TOKEN_TYPE_B = 2; // @dev Ensure the user's balances are much smaller than the pool's balance const POOL_UPPER_BOUND = 2 ** 30; const ACCOUNT_BALANCE_BOUND = 1073741; // (2 ** 30 / 1000) // // STORAGE VARIABLES // // @dev A map from account and token type to corresponding balance @storage_var func account_balance(account_id: felt, token_type: felt) -> (balance: felt){ } // @dev a map from token type to corresponding pool balance @storage_var func pool_balance(token_type: felt) -> (balance: felt) { } // // GETTERS // // @dev returns account balance for a given token // @param account_id Account to be queried // @param token_type Token to be queried @view func get_account_token_balance{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( account_id: felt, token_type: felt ) -> (balance: felt) { return account_balance.read(account_id, token_type); } // @dev return the pool's balance // @param token_type Token type to get pool balance @view func get_pool_token_balance{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( token_type: felt ) -> (balance: felt) { return pool_balance.read(token_type); } // // EXTERNALS // // @dev set pool balance for a given token // @param token_type Token whose balance is to be set // @param balance Amount to be set as balance @external func set_pool_token_balance{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( token_type: felt, balance: felt ) { with_attr error_message("exceeds maximum allowed tokens!"){ assert_nn_le(balance, BALANCE_UPPER_BOUND - 1); } pool_balance.write(token_type, balance); return (); } // @dev add demo token to the given account // @param token_a_amount amount of token a to be added // @param token_b_amount amount of token b to be added @external func add_demo_token{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( token_a_amount: felt, token_b_amount: felt ) { alloc_locals; let (account_id) = get_caller_address(); modify_account_balance(account_id=account_id, token_type=TOKEN_TYPE_A, amount=token_a_amount); modify_account_balance(account_id=account_id, token_type=TOKEN_TYPE_B, amount=token_b_amount); return (); } // @dev intialize AMM // @param token_a amount of token a to be set in pool // @param token_b amount of token b to be set in pool @external func init_pool{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( token_a: felt, token_b: felt ) { with_attr error_message("exceeds maximum allowed tokens!"){ assert_nn_le(token_a, POOL_UPPER_BOUND - 1); assert_nn_le(token_b, POOL_UPPER_BOUND - 1); } set_pool_token_balance(token_type=TOKEN_TYPE_A, balance=token_a); set_pool_token_balance(token_type=TOKEN_TYPE_B, balance=token_b); return (); } // @dev swaps token between the given account and the pool // @param token_from token to be swapped // @param amount_from amount of token to be swapped // @return amount_to the token swapped to @external func swap{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( token_from: felt, amount_from: felt ) -> (amount_to: felt) { alloc_locals; let (account_id) = get_caller_address(); // verify token_from is TOKEN_TYPE_A or TOKEN_TYPE_B with_attr error_message("token not allowed in pool!"){ assert (token_from - TOKEN_TYPE_A) * (token_from - TOKEN_TYPE_B) = 0; } // check requested amount_from is valid with_attr error_message("exceeds maximum allowed tokens!"){ assert_nn_le(amount_from, BALANCE_UPPER_BOUND - 1); } // check user has enough funds let (account_from_balance) = get_account_token_balance(account_id=account_id, token_type=token_from); with_attr error_message("insufficient balance!"){ assert_le(amount_from, account_from_balance); } let (token_to) = get_opposite_token(token_type=token_from); let (amount_to) = do_swap(account_id=account_id, token_from=token_from, token_to=token_to, amount_from=amount_from); return (amount_to=amount_to); } // // INTERNALS // // @dev internal function that updates account balance for a given token // @param account_id Account whose balance is to be modified // @param token_type Token type to be modified // @param amount Amount Amount to be added func modify_account_balance{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( account_id: felt, token_type: felt, amount: felt ) { let (current_balance) = account_balance.read(account_id, token_type); tempvar new_balance = current_balance + amount; with_attr error_message("exceeds maximum allowed tokens!"){ assert_nn_le(new_balance, BALANCE_UPPER_BOUND - 1); } account_balance.write(account_id=account_id, token_type=token_type, value=new_balance); return (); } // @dev internal function that swaps tokens between the given account and // the pool // @param account_id Account whose tokens are to be swapped // @param token_from Token type to be swapped from // @param token_to Token type to be swapped to // @param amount_from Amount to be swapped func do_swap{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( account_id: felt, token_from: felt, token_to: felt, amount_from: felt ) -> (amount_to: felt) { alloc_locals; // get pool balance let (local amm_from_balance) = get_pool_token_balance(token_type = token_from); let (local amm_to_balance) = get_pool_token_balance(token_type=token_to); // calculate swap amount let (local amount_to, _) = unsigned_div_rem((amm_to_balance * amount_from), (amm_from_balance + amount_from)); // update token_from balances modify_account_balance(account_id=account_id, token_type=token_from, amount=-amount_from); set_pool_token_balance(token_type=token_from, balance=(amm_from_balance + amount_from)); // update token_to balances modify_account_balance(account_id=account_id, token_type=token_to, amount=amount_to); set_pool_token_balance(token_type=token_to, balance=(amm_to_balance - amount_to)); return (amount_to=amount_to); } // @dev internal function to get the opposite token type // @param token_type Token whose opposite pair needs to be gotten func get_opposite_token(token_type: felt) -> (t: felt) { if(token_type == TOKEN_TYPE_A) { return (t=TOKEN_TYPE_B); } else { return (t=TOKEN_TYPE_A); } } ``` # Additional Resources 1. [Official documentation](https://www.cairo-lang.org/docs/) 2. [Starknet EDU](https://medium.com/starknet-edu) 3. [Journey through Cairo](https://medium.com/@darlingtonnnam/journey-through-cairo-i-setting-up-protostar-and-argentx-for-local-development-ba40ae6c5524) 4. [Demystifying Cairo whitepaper](https://medium.com/@pban/demystifying-cairo-white-paper-part-i-b71976ad0108) 5. [Learn about StarkNet with Argent](https://www.argent.xyz/learn/tag/starknet/) # Development Frameworks 1. [Protostar](https://docs.swmansion.com/protostar/docs/tutorials/installation) 2. [Nile](https://github.com/OpenZeppelin/nile) 3. [StarkNet CLI](https://www.cairo-lang.org/docs/quickstart.html) # Helpful Libraries 1. [Cairo-lang](https://github.com/starkware-libs/cairo-lang) 2. [Openzeppelin](https://github.com/OpenZeppelin/cairo-contracts) # Educational Repos 1. [StarkNet Cairo 101](https://github.com/starknet-edu/starknet-cairo-101) 2. [StarkNet ERC721](https://github.com/starknet-edu/starknet-erc721) 3. [StarkNet ERC20](https://github.com/starknet-edu/starknet-erc20) 4. [L1 -> L2 Messaging](https://github.com/starknet-edu/starknet-messaging-bridge) 5. [StarkNet Debug](https://github.com/starknet-edu/starknet-debug) 6. [StarkNet Accounts](https://github.com/starknet-edu/starknet-accounts) 7. [Min-Starknet](https://github.com/Darlington02/min-starknet) # Security 1. [Amarna static analysis for Cairo programs](https://blog.trailofbits.com/2022/04/20/amarna-static-analysis-for-cairo-programs/) 2. [Cairo and StarkNet security by Ctrl03](https://ctrlc03.github.io/) 3. [How to hack almost any Cairo smart contract](https://medium.com/ginger-security/how-to-hack-almost-any-starknet-cairo-smart-contract-67b4681ac0f6) 4. [Analyzing Cairo code using Armana](https://dic0de.substack.com/p/analyzing-cairo-code-using-amarna?sd=pf) # Future TO-DOs Update tutorial to fit Cairo 1.0