# Cairo Starknet contracts - Simple contract layout and style guide This is a simple style-guide we are using at Cygnus to re-write our core contracts in Cairo. But first, what is Cairo? Cairo is a programming language for writing provable programs, where one party can prove to another that a certain computation was executed correctly. Cairo and similar proof systems can be used to provide scalability to blockchains. StarkNet uses the Cairo programming language both for its infrastructure and for writing StarkNet contracts. Since our core contracts are based on Solidity and rely on the language's unique features, we have had to adopt a different mindset to transcribe the contracts. We encountered a few differences between the two, thus the purpose of this document. Our Solidity contracts were written with a very specific layout: >pragma solidity 0.8.4: >1. Libraries >2. Storage >3. Constructor >4. Modifiers >5. Constant functions >6. Non-Constant functions <br> | ![](https://i.imgur.com/NP1vdLF.png) |:--:| *Our Solidity core contracts layout* <br> Every single contract on our core (can be found here: https://github.com/CygnusDAO/cygnusdao-core) follows this practice. Starts with the `pragma solidity >= 0.8.4` and follows with the contract layout mentioned above. The contract instance from our core inherits all the structs/errors/events/functions from the interface, and they commit to implement all the functions found on there. In Cairo, **there are interfaces but there is no inheritance**. This means that we can't just rely on the interface itself to provide us with the basic structure of our contract. That's the first big difference. The second difference is that Cairo only supports **1 constructor per compiled contract** (ie. if a contract has multiple child contracts then there must only be 1 constructor defined across all). Our contracts rely on inheritance, and follow this path: <center> <img src="https://i.imgur.com/88Uo4cZ.png"> </center> <br> With collateral contracts inherting from CygLP, and borrow contracts inheriting from CygDAI and all the contracts have constructors. This can easily be resolved using an initializer instead of a constructor, so this is simple enough. The third and most important difference is data types and execution types: - There is only 1 data type: **felt**. A felt stands for an unsigned integer with up to 76 decimals. It is used to store addresses, strings, integers, etc. We declare it with: ``` magic_number : felt ``` ``` my_address : felt ``` - There is only 1 execution type: **func**. All storage variables (except structs) such as mappings, events, constructors and functions are declared with the same func keyword: ```javascript! @storage_var func this_is_single_storage_slot() -> (value : felt): end ``` ```javascript! @storage_var func this_is_a_mapping_slot(key : felt) -> (value : felt): end ``` ```javascript! @event func this_is_an_event(event_param1 : felt, event_param2 : felt): end ``` Since declaring everything wtih a `func` keyword can get confusing, Cairo provides **decorators** which are used on top of the function declarations to distinguish each. These all start with `@`: - **@event** - Represents an event - **@storage_var** - Represents a contract state variable (ie mapping, address, etc.) - **@constructor** - A constructor which runs once at deployment - **@view** - Used to *read* from storage_vars - **@external** - Used to *write* to storage_vars - **@l1_handler** - Used to to process a message sent from an L1 contract <br> # Layout Style We tried to follow our Solidity pattern of: `Storage -> Constructor -> Modifiers -> Constant Functions -> Non-Constant Functions` And this fits perfect with Cairo. We bring the structs and events into the contract itself (instead of the interfaces like with Solidity) and with the decorators we separate the contracts into the following: >%lang starknet >1. Structs >2. Events >3. Storage >4. Constructor >5. Storage Getters >6. Constant functions >7. Non-Constant functions <br> | ![](https://i.imgur.com/BVRMyQb.png) |:--:| *Our Cairo core contracts layout* <br> The layout is similar to our Solidity contracts, but we define the structs and events in the contract themselves, and add `Storage Getters` between constructors and constant functions, and that's pretty much it. Instead of pragma solidity, we first start the contract with `%lang starknet` and follow the pattern: `Structs -> Events -> Storage -> Constructor -> Storage Getters -> Constant functions -> Non-Constant Functions` The storage getters are necessary if you want to make them public. In solidity, the compiler creates getters for all state variables declared as public, in Cairo all @storage_vars are private. Thus if we want to make them public we must make a getter function ourselves. <br> # Naming Style Solidity follows the camelCase approach for every function and state variable, whether it is a uint256 or a string or an address. We then use PascalCase for structs, contracts and events which is the standard in Solidity but never camel case. Cairo follows the Python or Ruby approach of camel_case in and we followed this approach. The style we used is very similar to OnlyDust's but with a few changes (https://github.com/onlydustxyz/development-guidelines/blob/main/starknet/README.md). The main change is that we use Mixed_Case_Underscore for storage vars. Since we can't declare an event as `event {EventName}` or a mapping as `mapping`, we have to differentiate between all the funcs. Storage vars are the only ones which follow this approach along with library names, but the rest follow OnlyDust's conventions. We are still not sure if we will remain with this approach. Also, We customized the error message to see exactly where the error is coming from for debugging purposes, but that's pretty much it. |![](https://i.imgur.com/7dPn2bE.png) |:--:| *Our Cairo naming style* # Examples Single storage example: ```javascript! # 3. Storage @storage_var func Magic_Number() -> (number : felt){ } ---------------------------- # 5. Storage Getters @view func magic_number() -> (number : felt) { // Can return the read directly return Magic_Number.read(); } ---------------------------- # 6. Constant functions @view func multiply_magic_numbers(multiplier : felt) -> (res : felt) { // Read secret number from storage let (secret_number: felt) = Magic_Number_Read(); // Return result return (res=secret_number * multiplier); } ---------------------------- # 7. Non-Constant Functions // Notice the (value=) when we write. Value is the default arg for all storage_vars @external func new_magic_number(number : felt) -> (number : felt){ // write new number to storage return magic_number.write(value=number); } ``` Mapping storage example with a struct: ```javascript! // 1. Structs // This is how we create structs in Cairo 0.1 (member was deprecated) // @custom:member my_name user name // @custom:member my_age user age struct MyStruct { my_name: felt, my_age: felt } ---------------------------- // 2. Event // We can pass structs to events func MyEvent(new_struct : MyStruct): end ---------------------------- // 3. Storage // Mapping var to store structs @storage_var func Struct_Mapping(struct_id : felt) -> (my_struct : MyStruct): end ---------------------------- # 5. Storage getters // Read struct from storage @view func my_struct(struct_id : felt) -> (my_struct : MyStruct): return Struct_Mapping.read(struct_id=struct_id) end ---------------------------- # 7. Non-Constant Functions // Write new struct to storage @external func new_struct(id : felt, name : felt, age : felt){ // Must create a new struct to save to storage let my_new_struct = MyStruct(name=name, age=age); // Write to the mapping with the ID (ideally we'd use a unique identifier and a revert if already exists) Struct_Mapping.write(id, my_new_struct); // Emit event NewStruct.emit(my_struct); return (); } ``` # Cairo Tricks ## 1. Using simple returns for Cairo syscalls Using simple returns can help us simplify code readability and write the same in less lines of code. Simple returns in Cairo are only allowed for internal functions (externals only support tuples at the moment), but this is perfect when using custom-libraries or Cairo global variables such as `get_caller_address()` or `get_contract_address()` Example: ```javascript! // msg.sender func msg_sender{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> felt { let (caller_address: felt) = get_caller_address(); return caller_address; } // address(this) func address_this{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> felt { let (contract_address: felt) = get_contract_address(); return contract_address; } // block.timestamp func block_timestamp{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> felt { let (block_timestamp: felt) = get_block_timestamp(); return block_timestamp; } ``` Notice the simple `felt` return on the first line. This then allows us to use the address of the sender like in Solidity: ```javascript! @external func deposit{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( assets: Uint256, recipient: felt ) -> (shares: Uint256) { alloc_locals; ... // Transfer underlying asset from msg.sender to this contract let (underlying: felt) = Underlying.read(); // Transfer asset from caller SafeERC20.transferFrom( contract_address=underlying, sender=msg_sender(), recipient=address_this(), amount=assets ); ``` Without simple returns, in Cairo we would have to call each variable and then assign it like so: ```javascript! @external func deposit{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( assets: Uint256, recipient: felt ) -> (shares: Uint256) { alloc_locals; ... // Transfer underlying asset from msg.sender to this contract let (underlying: felt) = Underlying.read(); // Get caller let (msg_sender: felt) = get_caller_address(); // Get contract address let (address_this: felt) = get_contract_address(); // Transfer asset from caller SafeERC20.transferFrom( contract_address=underlying, sender=msg_sender, recipient=address_this, amount=assets ); ``` >context: https://github.com/CygnusDAO/starknet-factory/blob/main/src/cygnus_core/utils/context.cairo ## 2. Use battle-tested Math libraries (OZ, Math64x61) Writing own math libraries is fun, but can be deadly to many protocols. Unlike Solidity >= 0.8, Cairo calculations are still subject to overflow/underflow. As such, it is still needed to do sufficient checks on each calculation. Our contracts make use of popular Starknet libraries that have been tested and considered safe for use. The main math libraries we have used: >**OpenZeppelin** (covers 99% of cases): https://github.com/OpenZeppelin/cairo-contracts/blob/main/src/openzeppelin/security/safemath/library.cairo > >**Math64x61** (for more complex operations): https://github.com/influenceth/cairo-math-64x61/blob/master/contracts/cairo_math_64x61/math64x61.cairo In saying this, we have developed our own felt-based math library to use for our Oracle but it is NOT safe to use. It has only been tested in alpha-goerli and is NOT safe for use in production. We will most likely remove the library once we deploy on mainnet. ## 3. Pre-compute contract addresses (similar to CREATE2) Cairo has its own version of solidity's `CREATE2`. Taken from the release notes: > The address is a Pedersen hash on the caller address, a salt (random or chosen by the deployer), the contract code hash, and the hash of the constructor arguments, all appended by a prefix. This is a very important part of our contracts, since in 1 transaction we deploy 2 contracts (borrowable + collateral), and each must have each other's addresses in storage. To avoid having to deploy the contracts and then using a setter method on the contracts to store each other's addresses, we developed our own method: 1. Calculate the address of the collateral 2. Deploy the borrowable contract with **calculated** collateral address 3. Deploy the collateral contract with borrowable **deployed** address 4. Assert collateral calculated address is equal to deployed address While developing our contracts we could not find a real example out in the wild. Our factory contract makes use of this technique, so here is the full code we used: Factory contract: ```javascript= // 1. Calculate future collateral address // salt of lp token + factory let (salt: felt) = hash2{hash_ptr=pedersen_ptr}(lp_token_pair, address_this()); // Get hash from deployer contract let (collateral_class_hash: felt) = IDenebOrbiter.collateral_class_hash( contract_address=orbiter.deneb_orbiter ); // Allocate calldata let (constructor_calldata: felt*) = alloc(); // Calculate collateral address internally let (calculated_collateral: felt) = CygnusAddressLib.calculate_contract_address{ hash_ptr=pedersen_ptr }(salt, collateral_class_hash, 0, constructor_calldata, orbiter.deneb_orbiter); let (borrow_token: felt) = Dai.read(); // 2. Deploy borrowable with calculated collateral address // Deploy borrowable let (borrowable: felt) = IAlbireoOrbiter.deploy_borrowable( contract_address=orbiter.albireo_orbiter, collateral=calculated_collateral, underlying=borrow_token, shuttle_id=shuttle.shuttle_id, base_rate_per_year=base_rate_per_year, multiplier_per_year=multiplier_per_year, ); // 3. Deploy collateral with deployed borrowable address let (collateral: felt) = IDenebOrbiter.deploy_collateral( contract_address=orbiter.deneb_orbiter, borrowable=borrowable, underlying=lp_token_pair, shuttle_id=shuttle.shuttle_id, ); // 4. Check deployed collateral address vs calculated collateral address // // ERROR: collateral_address_mismatch // with_attr error_message("cygnus_factory__collateral_address_mismatch()") { // avoid if calculated collateral address is different than deployed assert collateral = calculated_collateral; } ``` Pre-compute contract library: ```javascript= namespace CygnusAddressLib { // // @notice Contract address prefix used for all contracts // const CONTRACT_ADDRESS_PREFIX = 'STARKNET_CONTRACT_ADDRESS'; // @notice Calculates a contract address on Starknet before deploying it // @param salt The salt for the fields that is being proved // @param class_hash The class hash of the contract we are deploying // @param constructor_calldata_size Size of the constructor params // @param constructor_calldata The constructor params // @param deployer_address The address of the deployer contract // @return contract_address The address of the contract func calculate_contract_address{hash_ptr: HashBuiltin*, range_check_ptr}( salt: felt, class_hash: felt, constructor_calldata_size: felt, constructor_calldata: felt*, deployer_address: felt, ) -> (contract_address: felt) { // // 1. Initialize a new HashState with no items // let (hash_state_ptr) = hash_init(); // // 2. Hash with the starknet contract address prefix // let (hash_state_ptr) = hash_update_single( hash_state_ptr=hash_state_ptr, item=CONTRACT_ADDRESS_PREFIX ); // // 3. Hash deployer address (the contract deploying it) // let (hash_state_ptr) = hash_update_single( hash_state_ptr=hash_state_ptr, item=deployer_address ); // // 4. Hash salt // let (hash_state_ptr) = hash_update_single(hash_state_ptr=hash_state_ptr, item=salt); // // 5. Hash the class hash of the contract we are deploying // let (hash_state_ptr) = hash_update_single(hash_state_ptr=hash_state_ptr, item=class_hash); // 6. Compute the hash of the following and then call hash_update_single to add to the hash_state // - hash_state_ptr // - calldata // - calldata size let (hash_state_ptr) = hash_update_with_hashchain( hash_state_ptr=hash_state_ptr, data_ptr=constructor_calldata, data_length=constructor_calldata_size, ); // 7. Returns the final hash result of the HashState let (contract_address_before_modulo) = hash_finalize(hash_state_ptr=hash_state_ptr); // 8. Normalize address (addr % ADDR_BOUND) so for a valid storage item address in the storage tree // https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/starknet/common/storage.cairo // let (contract_address) = normalize_address(addr=contract_address_before_modulo); // return contract address return (contract_address=contract_address); } } ``` At the time of writing the contracts we could not find contracts that implemented this method, however this was a few months ago. Maybe now someone has figured out a more concise way? For reference check out our contract library, factory contract and deployer contracts to see how this works: > **create2_address_lib**: https://github.com/CygnusDAO/starknet-factory/blob/main/src/cygnus_core/libraries/cygnus_pool_address.cairo > > **factory**: https://github.com/CygnusDAO/starknet-factory/blob/main/src/cygnus_core/giza_power_plant.cairo#L736 > >**borrowable_deployer**: https://github.com/CygnusDAO/starknet-factory/blob/main/src/cygnus_core/albireo_orbiter.cairo#L121 > >**collateral_deployer**: https://github.com/CygnusDAO/starknet-factory/blob/main/src/cygnus_core/deneb_orbiter.cairo#L115 ## 4. Reentrancy Guard Reentrancy attacks have drained so many protocols that is hard to keep count (actually pcaversaccio is keeping count on github, check it out! https://github.com/pcaversaccio/reentrancy-attacks) Our contracts rely heavily on reentrancy guards, as do (**should**) most lending protocols. Since Cairo has no modifiers, we make use of OZ' neat reentrancy guard solution for our borrow, repay, leverage and deleverage functions. Example: ```javascript! @external func liquidate{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( borrower: felt, liquidator: felt ) -> (cyg_lp_amount: Uint256) { alloc_locals; // Lock ReentrancyGuard._start(); ... // Unlock ReentrancyGuard._end(); // Return seized amount return (cyg_lp_amount=cyg_lp_amount); } ``` OZ' Reentrancy Guard: https://github.com/OpenZeppelin/cairo-contracts/blob/main/src/openzeppelin/security/reentrancyguard/library.cairo ## 5. Use Events and indexers Using events is the standard in EVMs for any serious protocol, specially when developing frontend libraries. Most blockchains don't provide an API to access contract's storage like a database. If we wish to do more complex queries or get historical data, then this becomes an issue. Indexers are tools that listen for the events emitted by the smart contracts and use them to update their view over the application's state. For Starknet we have been using [Apibara](https://www.apibara.com/), and we definitely recommend giving them a try. To use events, the process is similar to Solidity. First, we define the event in the contract: ```javascript! // @param sender Indexed address of msg.sender // @param recipient Indexed address of recipient // @param borrower Indexed address of the borrower // @param borrow_amount If borrow calldata, the amount of the underlying asset // @param repay_amount If repay calldata, the amount of the underlying // @param account_borrows_prior Record of borrower's total borrows before this // @param account_borrows Record of borrower's total borrows after this event // @param total_borrows_stored Record of the protocol's total borrows // @custom:event Borrow Logs when an account borrows or repays @event func Borrow( sender: felt, recipient: felt, borrower: felt, borrow_amount: Uint256, repay_amount: Uint256, account_borrows_prior: Uint256, account_borrows: Uint256, total_borrows_stored: Uint256, ) { } ``` Notice the empty body. Like storage_vars, the body must be left unimplemented. We can then emit the event in the function when a certain condition takes place (for example a successful `borrow`): ```javascript! @external func borrow{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( borrower: felt, recipient: felt, borrow_amount: Uint256, borrow_calldata: BorrowCallData ) { ... // // EVENT: Borrow // Borrow.emit( sender=msg_sender(), recipient=recipient, borrower=borrower, borrow_amount=borrow_amount, repay_amount=repay_amount, account_borrows_prior=account_borrows_prior, account_borrows=account_borrows, total_borrows_stored=total_borrows_stored, ); // Update total balance of underlying update_internal(); // Unlock ReentrancyGuard._end(); return (); } ``` ## 6. Custom Errors Solidity >= 0.8 allows to define custom errors, which in our opinion are a game changer. It reduces bytecode significantly by removing reason strings and allows to pass variables to see exactly why the transaction reverted. Well Cairo allows for the same thing! We can pass variables to our errors to see why the tx reverts. The caveat in this is that only variables which do not rely on AP are allowed to be passed to these errors. To define a custom error and revert a transaction when a condition is met, we simply pass an `error_message` attribute with a string and the parameters inside curly brackets {}. This will display the error in popular wallets such as ArgentX or Braavos with the variables passed: ```javascript! // @notice Erc4626 compatible deposit function, receives assets and mints shares to the recipient // @param assets Amount of assets to deposit to receive shares // @param recipient The address receiving shares // @return shares The amount of shares minted // @custom:security non-reentrant @external func deposit_terminal{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( assets: Uint256, recipient: felt ) -> (shares: Uint256) { alloc_locals; // lock ReentrancyGuard._start(); ... // ERROR: cant_mint_zero_shares with_attr error_message("cygnus_terminal__cant_mint_zero_shares({assets})") { // revert if shares is 0 assert_not_zero(shares.low + shares.high); } ... } ``` Notice that this is allowed as `assets` is passed as an argument and is not dependent on AP. ## 7. Make use of Empiric's market data feeds Since our contracts depend on asset prices to decide how much a user may borrow, we rely on our oracle to correctly price a liquidity position. Our first version of our oracle was a TWAP oracle, however as reliable as they can be there's still some limitations that comes with their use. In our case the biggest caveat was the delayed price of an asset can cause problems for lenders and borrowers at a time of high market volatility. Our latest oracle uses external price feeds instead of TWAPs to price liquidity positions. When migrating to Starknet we ended up using Empiric's price feeds, and integrating with them was really a breeze. To integrate with them, we have to only define 3 variables in our contracts and boom: ```javascript! // Oracle Interface Definition const EMPIRIC_ORACLE_ADDRESS = 0x012fadd18ec1a23a160cc46981400160fbf4a7a5eed156c4669e39807265bcd4; // Aggregation mode -> Median const AGGREGATION_MODE = 120282243752302; // The asset's unique Empiric key @storage_var func Oracle_Denomination_Token_Key() -> (oracle_denomination_token_key: felt) { } ``` Then to get for example the price of an asset we just call their contract: ```javascript! // @notice Getter for dai price (or denom_token) used for simplicity. // @return dai_price The price of dai from Empiric // @return dai_decimals The decimals used for this price feed, used to normalize later if need be @view func get_dai_price{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> ( dai_price: felt, dai_decimals: felt ) { // get Dai unique key we stored let (empiric_Key: felt) = Oracle_Denomination_Token_Key.read(); // get the price of Dai from Empiric let (dai_price, dai_decimals, _, _) = IEmpiricOracle.get_value( EMPIRIC_ORACLE_ADDRESS, empiric_Key, AGGREGATION_MODE ); // return current price of DAI in USD and decimals return (dai_price=dai_price, dai_decimals=dai_decimals); } ``` And that's it! Integrating with them was surprisingly simple. We suggest taking a look into their product, it's quite interesting and unique how they work. From their website: > Most existing oracle solutions operate as trusted, off-chain black boxes. Empiric's smart contracts leverage zk computation to aggregate raw signed data with total transparency and robustness. Because the oracle is entirely on-chain, anyone can verify any data point from origin through transformation to destination. Check them out here: https://docs.empiric.network/ Our oracle for reference: https://github.com/CygnusDAO/starknet-price-oracle/blob/main/src/cygnus_oracle/cygnus_nebula_oracle.cairo ## 8. More to come... We will keep updating as we update contracts (if needed) or find new techniques worth sharing. Thanks for reading!