Skip to content
This repository has been archived by the owner on Sep 25, 2024. It is now read-only.

Latest commit

 

History

History
190 lines (132 loc) · 8.66 KB

4. Contract Syntax.md

File metadata and controls

190 lines (132 loc) · 8.66 KB

Cairo Program, Starknet Contracts and Contract Syntax

In this section, we'll explore key syntaxes of the Cairo programming language, including differentiating between a Cairo program and a Starknet Contract!

Cairo Program

A Cairo program is a sequence of instructions written in the Cairo programming language that specifies a set of computations to be executed. The difference between these and Starknet contracts are Cairo programs do not have access to Starknet's VM and are as such stateless.

A program must always have a main entrypoint:

    fn main() {}

StarkNet Contract

A StarkNet contract, in very simple words are programs that runs on the Starknet VM. Since they run on the VM, they have access to Starknet’s persistent state, can alter or modify variables in Starknet’s states, communicate with other contracts, and interact seamlessly with the underlying L1.

Starknet contracts are denoted by the #[contract] macro.

Basic Contract Syntaxes

In the following code, we will analyze a simple ENS contract. This contract allows users to assign a name to their contract address. We will examine key concepts in the code such as imports, decorators, state variables, and how to read and write to the contract storage.

use starknet::ContractAddress;

#[starknet::interface]
trait IENS<TContractState> {
    fn store_name(ref self: TContractState, _name: felt252);
    fn get_name(self: @TContractState, _address: ContractAddress) -> felt252;
}

#[starknet::contract]
mod ENS {
    use starknet::get_caller_address;
    use starknet::ContractAddress;

    #[storage]
    struct Storage {
        names: LegacyMap::<ContractAddress, felt252>,
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        StoredName: StoredName
    }

    #[derive(Drop, starknet::Event)]
    struct StoredName {
        caller: ContractAddress,
        name: felt252
    }

    #[constructor]
    fn constructor(ref self: ContractState, _name: felt252, _address: ContractAddress) {
        self.names.write(_address, _name);
    }

    #[external(v0)]
    impl IENSImpl of super::IENS<ContractState> {
        fn store_name(ref self: ContractState, _name: felt252) {
            let caller = get_caller_address();
            self.names.write(caller, _name);
            self.emit(Event::StoredName(StoredName{ caller: caller, name: _name }));
        }

        fn get_name(self: @ContractState, _address: ContractAddress) -> felt252 {
            let name = self.names.read(_address);
            return name;
        }
    }
}

Attributes

In StarkNet contracts, attributes are special annotations that modify the behavior of certain functions or methods. Attributes are placed before a function or method and begin with the #[ symbol.

The followings are attributes commonly used in StarkNet contracts:

#[starknet::contract]: This attribute is used to mark a module as a StarkNet contract. It informs the compiler that the module should be compiled and deployed to the StarkNet network.

#[constructor]: This attribute allows the function to be called with any necessary arguments to initialize the contract's state variables on deployment.

#[storage]: This attribute is used to specify Storage structs.

#[starknet::interface]: This attribute is used to specify contract interfaces.

#[external]: This attribute is used to mark a function as an external function that can be called from outside the contract. External functions are typically used to modify the contract state or emit events.

#[event]: Defines events that can be emitted by the contract

#[derive()]: This attribute generates code to implement a default trait on the type you’ve annotated with the derive syntax.

#[l1_handler]: This decorator is used for functions which receives a message from L1.

NB: It's worthy to note that with the recent updated syntax changes, It's now compulsory for all contracts to have an Interface which is implemented within the contract body.

Imports

Imports are statements that allow the contract to use external modules and their associated functionality. Import statements are typically placed at the top of a contract file, before the module definition.

we use the use keyword to import the get_caller_address and ContractAddress from starknet's core library. The get_caller_address method is used to retrieve the address of the caller and ContractAddress generates the address type. By importing these functions, we make them available for use in our contract without having to define them ourselves.

    use starknet::get_caller_address;
    use starknet::ContractAddress;

Functions

Functions in Cairo 1.0 looks similar to the 0.x versions, with the omission of the implicit arguments and the change of the func keyword to fn following Rust’s convention.

With the new Cairo version, the external and view attributes are also no longer necessary as you can differentiate an external function from a view function by wether they accept a reference or a snapshot of the self variable. external function accepts a reference to self, why view functions accept a snapshot of self (owing to the fact they shouldn't be able to mutate the Contract state).

Here’s an example of a basic external function signature in Cairo 1.0:

    fn store_name(ref self: ContractState, _name: felt252) {
        ...
    }

Here’s an example of a basic view function signature in Cairo 1.0:

    fn get_name(self: @ContractState, _address: ContractAddress) -> felt252 {
        ...
    }

Functions can also be annotated with specific attributes such as #[constructor], #[l1_handler] etc, depending on the use case.

State Variables

State variables are variables that store data that can be accessed and modified throughout the lifetime of the contract. These variables are declared within the struct block of a module and are stored in the contract's persistent storage. State variables are stored in a struct named Storage.

    #[storage]
    struct Storage{
        age: u8,
        names: LegacyMap::<ContractAddress, felt252>,
    }

Mappings as you can see, are created using the LegacyMap keyword, and specifying the data types of the mapped variables within <>. You can create more complex mappings using tuples e.g

    allowances: LegacyMap::<(ContractAddress, ContractAddress), u256>

which is the mapping of (owner, spender) -> balances for an ERC20 token.

Writing to storage

    fn store_name(ref self: ContractState, _name: felt252) {
        let caller = get_caller_address();
        self.names.write(caller, _name);
        self.emit(Event::StoredName(StoredName{ caller: caller, name: _name }));
    }

The store_name function is used to write a name to the contract storage for a given address. Here's how this function works:

  • The get_caller_address function is used to retrieve the address of the caller, which is then stored in the caller variable.
  • The self.names.write(caller, _name) statement writes the given _name value to the contract storage for the caller address. The names variable refers to a state variable of the Storage struct that stores the mapping of names to contract addresses.
  • The self.emit(Event::StoredName(StoredName{ caller: caller, name: _name })) statement emits a StoredName event with the caller address and the _name value as arguments. This event is emitted to the StarkNet network, allowing other contracts and external clients to receive notifications when a name is stored in the contract storage.

Overall, the store_name function allows a user to specify a name for their contract address and store it in the contract storage. By writing the name to the contract storage.

Reading from storage

    fn get_name(self: @ContractState, _address: ContractAddress) -> felt252 {
        let name = self.names.read(_address);
        return name;
    }
  • The get_name function is used to read a name from the contract storage for a given contract address. Here's how this function works:

  • The self parameter holds a snapshot of the Contract State.

  • The _address parameter specifies the contract address for which to retrieve the name.

  • The self.names.read(_address) statement reads the name from the contract storage for the specified _address. The names variable refers to a state variable of the Storage struct that stores the mapping of names to contract addresses.

  • The name variable stores the value that was read from the contract storage.

  • Finally, the name variable is returned to the caller of the function.

PS: For now, ignore the events, we'll look into that in details in chapter 8