- Project/Directory
- Interfaces
- Interfaces should be named with the prefix I and the name should be written in PascalCasing i.e. IAccount .
- Interfaces should be defined in an
interfaces/subfolder within the module directory (e.g.,src/branch/interfaces/IActivePool.cairofor the ActivePool contract insrc/branch/). - The generic state parameter should be named TState.
- Functions in interfaces should be grouped by category: write functions first, then view functions.
- Within each category (write/view), functions should be ordered alphabetically.
- Public structs that appear in interface signatures should be defined in the interface file, not in the contract file.
- Contracts
- Declaration Order and Section Organization
- Each major section in a contract should be visually separated using comment blocks with forward slashes (e.g.,
////////////////////////////////////////////////////////////////) - Section headers should be centered within the delimiter blocks with consistent spacing
- The order of sections and their standard names:
- Imports (no delimiter needed at top of file)
COMPONENT DECLARATIONS- For component! macro declarationsCOMPONENT IMPORTS- For component implementation imports and exposuresCONSTANTS- For constant declarationsSTORAGE- For storage struct definitionSTRUCTS- For custom struct definitionsENUMS- For custom enum definitions (excluding the main Event enum)EVENTS- For event enum and event struct definitionsCONSTRUCTOR- For the contract constructor (if present)ADDITIONAL IMPLEMENTATIONS- For supplementary trait implementations (e.g., SNIP12Metadata, access control traits, etc.)PUBLIC FUNCTIONSorINTERFACE FUNCTIONS- For the main interface implementation (write functions first, then view functions, in the same order as declared in the interface file)INTERNAL FUNCTIONS- For internal implementation block with#[generate_trait](sorted by logical order, then alphabetical order)UTILITY FUNCTIONS- For standalone functions that don't depend on contract state (placed at bottom, outside any impl block)
- Within function implementation blocks (both interface and internal), use sub-sections:
WRITE FUNCTIONS- For functions that modify contract stateREAD FUNCTIONS- For read-only functions that don't modify state (view/getter functions)
- No internal function should be left free; they should all be in an InternalImpl block.
- Example structure:
//////////////////////////////////////////////////////////////// // COMPONENT DECLARATIONS // //////////////////////////////////////////////////////////////// component!(...) //////////////////////////////////////////////////////////////// // COMPONENT IMPORTS // //////////////////////////////////////////////////////////////// #[abi(embed_v0)] impl ComponentImpl = ... //////////////////////////////////////////////////////////////// // CONSTANTS // //////////////////////////////////////////////////////////////// const MY_CONSTANT: u256 = 100; //////////////////////////////////////////////////////////////// // STORAGE // //////////////////////////////////////////////////////////////// #[storage] struct Storage { ... } //////////////////////////////////////////////////////////////// // STRUCTS // //////////////////////////////////////////////////////////////// #[derive(Drop, Serde)] struct MyCustomStruct { ... } //////////////////////////////////////////////////////////////// // ENUMS // //////////////////////////////////////////////////////////////// #[derive(Drop, Serde)] enum MyCustomEnum { ... } //////////////////////////////////////////////////////////////// // EVENTS // //////////////////////////////////////////////////////////////// #[event] #[derive(Drop, starknet::Event)] enum Event { MyEvent: MyEvent, } #[derive(Drop, starknet::Event)] struct MyEvent { ... } //////////////////////////////////////////////////////////////// // CONSTRUCTOR // //////////////////////////////////////////////////////////////// #[constructor] fn constructor(ref self: ContractState, owner: ContractAddress) { self.owner.write(owner); } //////////////////////////////////////////////////////////////// // ADDITIONAL IMPLEMENTATIONS // //////////////////////////////////////////////////////////////// #[abi(embed_v0)] impl SNIP12MetadataImpl of ISNIP12Metadata<ContractState> { fn name(self: @ContractState) -> felt252 { ... } fn version(self: @ContractState) -> felt252 { ... } } //////////////////////////////////////////////////////////////// // PUBLIC FUNCTIONS // //////////////////////////////////////////////////////////////// #[abi(embed_v0)] impl IMyContractImpl of IMyContract<ContractState> { //////////////////////////////////////////////////////////////// // WRITE FUNCTIONS // //////////////////////////////////////////////////////////////// fn set_value(ref self: ContractState, value: u256) { ... } //////////////////////////////////////////////////////////////// // READ FUNCTIONS // //////////////////////////////////////////////////////////////// fn get_value(self: @ContractState) -> u256 { ... } } //////////////////////////////////////////////////////////////// // INTERNAL FUNCTIONS // //////////////////////////////////////////////////////////////// #[generate_trait] impl InternalImpl of InternalTrait { //////////////////////////////////////////////////////////////// // WRITE FUNCTIONS // //////////////////////////////////////////////////////////////// fn update_internal_state(ref self: ContractState) { ... } //////////////////////////////////////////////////////////////// // READ FUNCTIONS // //////////////////////////////////////////////////////////////// fn calculate_internal_value(self: @ContractState) -> u256 { ... } } //////////////////////////////////////////////////////////////// // UTILITY FUNCTIONS // //////////////////////////////////////////////////////////////// fn calculate_hash(value: u256) -> felt252 { ... }
- Each major section in a contract should be visually separated using comment blocks with forward slashes (e.g.,
- Imports
- Imports should be listed alphabetically (
scarb fmtautomatically does this). - When bringing multiple elements into scope from one source, favor limiting each use clause to 3 lines total.
- Group imported elements in a logical way e.g. don't import a component, event, and function in the same use statement.
- Imports should be listed alphabetically (
- Constants
- Constant variables should be written in SCREAMING_SNAKE_CASE.
- Storage
- Storage variables should be prefixed with the component name.
- Events
- An Events enum should list all component events.
- Each component event should be defined as a struct.
- Event names should be written in PascalCasing.
- Event parameters should be written in snake_case.
- The #[key] annotation should be added above the event parameter if that parameter should be indexed.
- Impl
- Implementations should follow the naming pattern of:
- Remove the I prefix for the impl name i.e.
IERC20 ⇒ ERC20. - Add the Impl suffix as the embeddable impl name i.e. IERC20 ⇒ ERC20Impl.
- Remove the I prefix for the impl name i.e.
- If the name of the Trait finishes with Trait, then the impl doesn’t have to end with Impl
- Internal implementations should be named as InternalImpl.
- Implementations should follow the naming pattern of:
- Functions/Casings
- Functions should be written in snake_case.
- The only exception to this is when defining dual case dispatchers.
- Underscore prefix (
_) usage:- By default, all functions should be written without underscore prefix
- Use
_prefix ONLY to disambiguate when there's a naming conflict between:- An embeddable trait function and an internal implementation function
- Example:
transfer()in embeddable trait calls_transfer()in InternalImpl
- The underscore has no visibility meaning - it's purely for disambiguation
- Standalone functions that don't depend on contract state:
- If used only within the current contract: place outside impl blocks as regular functions
- If used across multiple contracts: place in common file (e.g.
utils.cairo)
- Error Messages
- Error messages should always start with a module prefix (e.g., ActivePool errors should start with 'AP: ')
- Use consistent short prefixes for each module to stay within the 31 character limit
- Documentation
- Developer-related documentation should appear in the code implementation
- General user-facing documentation should appear in the interface definitions
- Declaration Order and Section Organization
- Testing
- Test functions should follow the naming pattern
test_PREFIX_*where PREFIX is a shorthand for the module being tested- Example: ActivePool tests should be named
test_ap_* - This enables running module-specific tests with
scarb test test_PREFIX
- Example: ActivePool tests should be named
- Use consistent prefixes across all tests for the same module
- Organize tests logically: success cases, failure cases, edge cases
- Test functions should follow the naming pattern
- File Naming
- Contract files should be named in PascalCase (e.g.,
ActivePool.cairo) - All other files should be named in snake_case (e.g.,
test_helpers.cairo)
- Contract files should be named in PascalCase (e.g.,
- Code Optimization
- Prioritize code readability over gas optimization
- Only apply gas optimizations when they provide significant improvements
- Document any non-obvious optimizations with comments explaining the trade-off
- Interfaces