Starknet by Example

Starknet By Ejemplo es una colección de ejemplos de cómo utilizar el lenguaje de programación de Cairo para crear smart contracts en Starknet.

Starknet es un Validity-Rollup sin permiso que admite el cálculo general. Actualmente se utiliza como capa 2 de Ethereum. Starknet utiliza el sistema de prueba criptográfica STARK para garantizar una alta seguridad y escalabilidad.

Los smart contracts de Starknet están escritos en el idioma de Cairo. Cairo es un lenguaje de programación completo de Turing diseñado para escribir programas demostrables, abstrayendo el sistema de prueba zk-STARK del programador.

The current version of this book use:

scarb 2.3.1

For whom is this for?

Starknet By Ejemplo es para cualquiera que quiera aprender rápidamente cómo escribir contratos inteligentes en Starknet usando Cairo con cierta experiencia técnica en programación y blockchain.

The first chapters will give you a basic understanding of the Cairo programming language and how to write, deploy and use smart contracts on Starknet. The later chapters will cover more advanced topics and show you how to write more complex smart contracts.

Further reading

If you want to learn more about the Cairo programming language, you can read the Cairo Book. If you want to learn more about Starknet, you can read the Starknet documentation and the Starknet Book.

Aquí hay una lista de otros recursos que pueden resultarle útiles:

  • Starklings: An interactive tutorial to get you up and running with Cairo v1 and Starknet
  • Cairopractice: A blog with a series of articles about Cairo and Starknet
  • Cairo by example: An introduction to Cairo, with simple examples
Last change: 2023-12-06, commit: 1af1816

Basics of Smart Contracts in Cairo

Los siguientes capítulos le presentarán los smart contracts de Starknet y cómo escribirlos en Cairo.

Last change: 2023-10-12, commit: 90aa7c0

Storage

Este es el contrato mínimo que puedes redactar en Cairo:

#[starknet::contract]
mod Contract {
    #[storage]
    struct Storage {}
}

Storage is a struct annoted with #[storage]. Every contract must have one and only one storage. It's a key-value store, where each key will be mapped to a storage address of the contract's storage space.

Puede definir variables de storage en su contrato y luego usarlas para almacenar y recuperar datos.

#[starknet::contract]
mod Contract {
    #[storage]
    struct Storage {
        a: u128,
        b: u8,
        c: u256
    }
}

Actually these two contracts have the same underlying sierra program. From the compiler's perspective, the storage variables don't exist until they are used.

También puede leer sobre almacenamiento de tipos personalizados

Last change: 2023-11-20, commit: 3890c7b

Constructor

Los constructores son un tipo especial de función que se ejecuta solo una vez al implementar un contrato y se pueden usar para inicializar el estado del contrato. Su contrato no debe tener más de un constructor, y esa función constructora debe estar anotada con el atributo #[constructor]. Además, una buena práctica consiste en denominar a esa función constructor.

A continuación se muestra un ejemplo sencillo que demuestra cómo inicializar el estado de un contrato durante la implementación definiendo la lógica dentro de un constructor.

#[starknet::contract]
mod ExampleConstructor {
    use starknet::ContractAddress;

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

    // The constructor is decorated with a `#[constructor]` attribute.
    // It is not inside an `impl` block.
    #[constructor]
    fn constructor(ref self: ContractState, name: felt252, address: ContractAddress) {
        self.names.write(address, name);
    }
}

Visita el contrato en Voyager o juega con él en Remix.

Last change: 2023-10-12, commit: 90aa7c0

Variables

Hay 3 tipos de variables en los contratos de Cairo:

  • Local
    • declared inside a function
    • not stored on the blockchain
  • Storage
    • declared in the Storage of a contract
    • can be accessed from one execution to another
  • Global
    • provides information about the blockchain
    • accessed anywhere, even within library functions

Local Variables

Las variables locales se utilizan y se accede a ellas dentro del alcance de una función o bloque de código específico. Son temporales y existen solo mientras dure esa función en particular o la ejecución del bloque.

Las variables locales se almacenan en la memoria y no en la cadena de bloques. Esto significa que no se puede acceder a ellos de una ejecución a otra. Las variables locales son útiles para almacenar datos temporales que son relevantes sólo dentro de un contexto específico. También hacen que el código sea más legible al dar nombres a los valores intermedios.

Aquí hay un ejemplo simple de un contrato con solo variables locales:

#[starknet::interface]
trait ILocalVariablesExample<TContractState> {
    fn do_something(self: @TContractState, value: u32) -> u32;
}

#[starknet::contract]
mod LocalVariablesExample {
    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl LocalVariablesExample of super::ILocalVariablesExample<ContractState> {
        fn do_something(self: @ContractState, value: u32) -> u32 {
            // This variable is local to the current block. It can't be accessed once it goes out of scope.
            let increment = 10;

            {
                // The scope of a code block allows for local variable declaration
                // We can access variables defined in higher scopes.
                let sum = value + increment;
                sum
            }
        }
    }
}

Visite el contrato en Voyager](https://goerli.voyager.online/contract/0x015B3a10F9689BeD741Ca3C210017BC097122CeF76f3cAA191A20ff8b9b56b96) o juegue con él en Remix.

Storage Variables

Las variables de almacenamiento (storage) son datos persistentes almacenados en la blockchain. Se puede acceder a ellos de una ejecución a otra, lo que permite que el contrato recuerde y actualice la información a lo largo del tiempo.

Para escribir o actualizar una variable de storage, debe interactuar con el contrato a través de un punto de entrada externo mediante el envío de una transacción.

Por otro lado, puedes leer variables de estado, de forma gratuita, sin ninguna transacción, simplemente interactuando con un nodo.

A continuación se muestra un ejemplo sencillo de un contrato con una variable de almacenamiento:

#[starknet::interface]
trait IStorageVariableExample<TContractState> {
    fn set(ref self: TContractState, value: u32);
    fn get(self: @TContractState) -> u32;
}
#[starknet::contract]
mod StorageVariablesExample {
    // All storage variables are contained in a struct called Storage
    // annotated with the `#[storage]` attribute
    #[storage]
    struct Storage {
        // Storage variable holding a number
        value: u32
    }

    #[abi(embed_v0)]
    impl StorageVariablesExample of super::IStorageVariableExample<ContractState> {
        // Write to storage variables by sending a transaction that calls an external function
        fn set(ref self: ContractState, value: u32) {
            self.value.write(value);
        }

        // Read from storage variables without sending transactions
        fn get(self: @ContractState) -> u32 {
            self.value.read()
        }
    }
}

Visita el contrato en Voyager o juega con él en Remix.

Global Variables

Las variables globales son variables predefinidas que proporcionan información sobre la blockchain y el entorno de ejecución actual. ¡Se puede acceder a ellos en cualquier momento y desde cualquier lugar!

En Starknet, puede acceder a variables globales utilizando funciones específicas contenidas en las bibliotecas principales de Starknet.

Por ejemplo, la función get_caller_address devuelve la dirección de la persona que llama de la transacción actual, y la función get_contract_address devuelve la dirección del contrato actual.

#[starknet::interface]
trait IGlobalExample<TContractState> {
    fn foo(ref self: TContractState);
}
#[starknet::contract]
mod GlobalExample {
    // import the required functions from the starknet core library
    use starknet::get_caller_address;

    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl GlobalExampleImpl of super::IGlobalExample<ContractState> {
        fn foo(ref self: ContractState) {
            // Call the get_caller_address function to get the sender address
            let caller = get_caller_address();
        // ...
        }
    }
}

Visite el contrato en Voyager](https://goerli.voyager.online/contract/0x05bD2F3943bd4e030f85678b55b2EC2C1be939e32388530FB20ED967B3Be433F) o juega con él en Remix.

Last change: 2023-11-20, commit: 3890c7b

Visibility and Mutability

Visibility

Hay dos tipos de funciones en los contratos Starknet:

  • Functions that are accessible externally and can be called by anyone.
  • Functions that are only accessible internally and can only be called by other functions in the contract.

Estas funciones también suelen dividirse en dos bloques de implementación diferentes. El primer bloque impl para funciones accesibles externamente está anotado explícitamente con un atributo #[abi(embed_v0)]. Esto indica que todas las funciones dentro de este bloque se pueden llamar como una transacción o como una función de view. El segundo bloque impl para funciones accesibles internamente no está anotado con ningún atributo, lo que significa que todas las funciones dentro de este bloque son privadas de forma predeterminada.

State Mutability

Regardless of whether a function is internal or external, it can either modify the contract's state or not. When we declare functions that interact with storage variables inside a smart contract, we need to explicitly state that we are accessing the ContractState by adding it as the first parameter of the function. This can be done in two different ways:

  • If we want our function to be able to mutate the state of the contract, we pass it by reference like this: ref self: ContractState.
  • If we want our function to be read-only and not mutate the state of the contract, we pass it by snapshot like this: self: @ContractState.

Read-only functions, also called view functions, can be directly called without making a transaction. You can interact with them directly through a RPC node to read the contract's state, and they're free to call! External functions, that modify the contract's state, on the other side can only be called by making a transaction.

Las funciones internas no se pueden llamar externamente, pero se aplica el mismo principio con respecto a la mutabilidad de estado.

Echemos un vistazo a un contrato de ejemplo simple para verlos en acción:

#[starknet::interface]
trait IExampleContract<TContractState> {
    fn set(ref self: TContractState, value: u32);
    fn get(self: @TContractState) -> u32;
}

#[starknet::contract]
mod ExampleContract {
    #[storage]
    struct Storage {
        value: u32
    }


    // The `abi(embed_v0)` attribute indicates that all the functions in this implementation can be called externally.
    // Omitting this attribute would make all the functions in this implementation internal.
    #[abi(embed_v0)]
    impl ExampleContract of super::IExampleContract<ContractState> {
        // The `set` function can be called externally because it is written inside an implementation marked as `#[external]`.
        // It can modify the contract's state as it is passed as a reference.
        fn set(ref self: ContractState, value: u32) {
            self.value.write(value);
        }

        // The `get` function can be called externally because it is written inside an implementation marked as `#[external]`.
        // However, it can't modify the contract's state is passed as a snapshot: it is only a "view" function.
        fn get(self: @ContractState) -> u32 {
            // We can call an internal function from any functions within the contract
            PrivateFunctionsTrait::_read_value(self)
        }
    }

    // The lack of the `external` attribute indicates that all the functions in this implementation can only be called internally.
    // We name the trait `PrivateFunctionsTrait` to indicate that it is an internal trait allowing us to call internal functions.
    #[generate_trait]
    impl PrivateFunctions of PrivateFunctionsTrait {
        // The `_read_value` function is outside the implementation that is marked as `#[abi(embed_v0)]`, so it's an _internal_ function
        // and can only be called from within the contract.
        // However, it can't modify the contract's state is passed as a snapshot: it is only a "view" function.
        fn _read_value(self: @ContractState) -> u32 {
            self.value.read()
        }
    }
}

Visita el contrato en Voyager o juega con él en Remix.

Last change: 2023-10-19, commit: 3e4c697

Simple Counter

Este es un contrato de un simple counter.

Así es como trabaja:

  • The contract has a state variable called 'counter' that is initialized to 0.

  • When a user calls 'increment', the contract increments the counter by 1.

  • When a user calls 'decrement', the contract decrements the counter by 1.

#[starknet::interface]
trait ISimpleCounter<TContractState> {
    fn get_current_count(self: @TContractState) -> u128;
    fn increment(ref self: TContractState);
    fn decrement(ref self: TContractState);
}

#[starknet::contract]
mod SimpleCounter {
    #[storage]
    struct Storage {
        // Counter variable
        counter: u128,
    }

    #[constructor]
    fn constructor(ref self: ContractState, init_value: u128) {
        // Store initial value
        self.counter.write(init_value);
    }

    #[abi(embed_v0)]
    impl SimpleCounter of super::ISimpleCounter<ContractState> {
        fn get_current_count(self: @ContractState) -> u128 {
            return self.counter.read();
        }

        fn increment(ref self: ContractState) {
            // Store counter value + 1
            let counter = self.counter.read() + 1;
            self.counter.write(counter);
        }
        fn decrement(ref self: ContractState) {
            // Store counter value - 1
            let counter = self.counter.read() - 1;
            self.counter.write(counter);
        }
    }
}

Visite el contrato en Voyager o juega con él en Remix.

Last change: 2023-11-04, commit: d1889be

Mappings

Los mapas son una estructura de datos de key-value que se utiliza para almacenar datos dentro de un contrato inteligente. En Cairo se implementan utilizando el tipo LegacyMap. Es importante tener en cuenta que el tipo LegacyMap solo se puede usar dentro de la estructura Storage de un contrato y no se puede usar en ningún otro lugar.

Here we demonstrate how to use the LegacyMap type within a Cairo contract, to map between a key of type ContractAddress and value of type felt252. The key-value types are specified within angular brackets <>. We write to the map by calling the write() method, passing in both the key and value. Similarly, we can read the value associated with a given key by calling the read() method and passing in the relevant key.

Algunas notas adicionales:

  • More complex key-value mappings are possible, for example we could use LegacyMap::<(ContractAddress, ContractAddress), felt252> to create an allowance on an ERC20 token contract.

  • In mappings, the address of the value at key k_1,...,k_n is h(...h(h(sn_keccak(variable_name),k_1),k_2),...,k_n) where is the Pedersen hash and the final value is taken mod2251−256. You can learn more about the contract storage layout in the Starknet Documentation

use starknet::ContractAddress;

#[starknet::interface]
trait IMapContract<TContractState> {
    fn set(ref self: TContractState, key: ContractAddress, value: felt252);
    fn get(self: @TContractState, key: ContractAddress) -> felt252;
}

#[starknet::contract]
mod MapContract {
    use starknet::ContractAddress;

    #[storage]
    struct Storage {
        // The `LegacyMap` type is only available inside the `Storage` struct.
        map: LegacyMap::<ContractAddress, felt252>,
    }

    #[abi(embed_v0)]
    impl MapContractImpl of super::IMapContract<ContractState> {
        fn set(ref self: ContractState, key: ContractAddress, value: felt252) {
            self.map.write(key, value);
        }

        fn get(self: @ContractState, key: ContractAddress) -> felt252 {
            self.map.read(key)
        }
    }
}

Visita el contrato en Voyager](https://goerli.voyager.online/contract/0x06214AB4c23Cc545bf2221D465eB83aFb7412779AD498BD48a724B3F645E3505) o juega con él en Remix.

Last change: 2023-10-12, commit: 90aa7c0

Errors

Errors can be used to handle validation and other conditions that may occur during the execution of a smart contract. If an error is thrown during the execution of a smart contract call, the execution is stopped and any changes made during the transaction are reverted.

Para generar un error, use las funciones assert o panic:

  • assert is used to validate conditions. If the check fails, an error is thrown along with a specified value, often a message. It's similar to the require statement in Solidity.

  • panic immediately halt the execution with the given error value. It should be used when the condition to check is complex and for internal errors. It's similar to the revert statement in Solidity. (Use panic_with_felt252 to be able to directly pass a felt252 as the error value)

Aquí hay un ejemplo simple que demuestra el uso de estas funciones:

#[starknet::interface]
trait IErrorsExample<TContractState> {
    fn test_assert(self: @TContractState, i: u256);
    fn test_panic(self: @TContractState, i: u256);
}
#[starknet::contract]
mod ErrorsExample {
    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl ErrorsExample of super::IErrorsExample<ContractState> {
        fn test_assert(self: @ContractState, i: u256) {
            // Assert used to validate a condition
            // and abort execution if the condition is not met
            assert(i > 0, 'i must be greater than 0');
        }

        fn test_panic(self: @ContractState, i: u256) {
            if (i == 0) {
                // Panic used to abort execution directly
                panic_with_felt252('i must not be 0');
            }
        }
    }
}

Visita el contrato en Voyager o juega con él en Remix.

Custom errors

Puede facilitar el manejo de errores definiendo sus códigos de error en un módulo específico.

mod Errors {
    const NOT_POSITIVE: felt252 = 'must be greater than 0';
    const NOT_NULL: felt252 = 'must not be null';
}

#[starknet::interface]
trait ICustomErrorsExample<TContractState> {
    fn test_assert(self: @TContractState, i: u256);
    fn test_panic(self: @TContractState, i: u256);
}

#[starknet::contract]
mod CustomErrorsExample {
    use super::Errors;

    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl CustomErrorsExample of super::ICustomErrorsExample<ContractState> {
        fn test_assert(self: @ContractState, i: u256) {
            assert(i > 0, Errors::NOT_POSITIVE);
        }

        fn test_panic(self: @ContractState, i: u256) {
            if (i == 0) {
                panic_with_felt252(Errors::NOT_NULL);
            }
        }
    }
}

Visita el contrato en Voyager o juega con él en Remix.

Vault example

Aquí hay otro ejemplo que demuestra el uso de errores en un contrato más complejo:

mod VaultErrors {
    const INSUFFICIENT_BALANCE: felt252 = 'insufficient_balance';
// you can define more errors here
}

#[starknet::interface]
trait IVaultErrorsExample<TContractState> {
    fn deposit(ref self: TContractState, amount: u256);
    fn withdraw(ref self: TContractState, amount: u256);
}

#[starknet::contract]
mod VaultErrorsExample {
    use super::VaultErrors;

    #[storage]
    struct Storage {
        balance: u256,
    }

    #[abi(embed_v0)]
    impl VaultErrorsExample of super::IVaultErrorsExample<ContractState> {
        fn deposit(ref self: ContractState, amount: u256) {
            let mut balance = self.balance.read();
            balance = balance + amount;
            self.balance.write(balance);
        }

        fn withdraw(ref self: ContractState, amount: u256) {
            let mut balance = self.balance.read();

            assert(balance >= amount, VaultErrors::INSUFFICIENT_BALANCE);

            // Or using panic:
            if (balance >= amount) {
                panic_with_felt252(VaultErrors::INSUFFICIENT_BALANCE);
            }

            let balance = balance - amount;

            self.balance.write(balance);
        }
    }
}

Visita el contrato en Voyager o juega con él en Remix.

Last change: 2023-10-12, commit: 90aa7c0

Events

Events are a way to emit data from a contract. All events must be defined in the Event enum, which must be annotated with the #[event] attribute. An event is defined as struct that derives the #[starknet::Event] trait. The fields of that struct correspond to the data that will be emitted. An event can be indexed for easy and fast access when querying the data at a later time. Events data can be indexed by adding a #[key] attribute to a field member.

A continuación se muestra un ejemplo simple de un contrato que utiliza eventos que emiten un evento cada vez que la función increment incrementa un contador:

#[starknet::interface]
trait IEventCounter<TContractState> {
    fn increment(ref self: TContractState);
}
#[starknet::contract]
mod EventCounter {
    use starknet::{get_caller_address, ContractAddress};
    #[storage]
    struct Storage {
        // Counter value
        counter: u128,
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    // The event enum must be annotated with the `#[event]` attribute.
    // It must also derive the `Drop` and `starknet::Event` traits.
    enum Event {
        CounterIncreased: CounterIncreased,
        UserIncreaseCounter: UserIncreaseCounter
    }

    // By deriving the `starknet::Event` trait, we indicate to the compiler that
    // this struct will be used when emitting events.
    #[derive(Drop, starknet::Event)]
    struct CounterIncreased {
        amount: u128
    }

    #[derive(Drop, starknet::Event)]
    struct UserIncreaseCounter {
        // The `#[key]` attribute indicates that this event will be indexed.
        #[key]
        user: ContractAddress,
        new_value: u128,
    }

    #[abi(embed_v0)]
    impl EventCounter of super::IEventCounter<ContractState> {
        fn increment(ref self: ContractState) {
            let mut counter = self.counter.read();
            counter += 1;
            self.counter.write(counter);
            // Emit event
            self.emit(Event::CounterIncreased(CounterIncreased { amount: 1 }));
            self
                .emit(
                    Event::UserIncreaseCounter(
                        UserIncreaseCounter {
                            user: get_caller_address(), new_value: self.counter.read()
                        }
                    )
                );
        }
    }
}

Visita el contrato en Voyager ojuega con él en Remix.

Last change: 2023-10-12, commit: 90aa7c0

Storing Custom Types

Si bien los tipos nativos se pueden almacenar en el almacenamiento de un contrato sin ningún trabajo adicional, los tipos personalizados requieren un poco más de trabajo. Esto se debe a que en el momento de la compilación, el compilador no sabe cómo almacenar tipos personalizados en el almacenamiento. Para resolver esto, necesitamos implementar el trait Storepara nuestro tipo personalizado. Con suerte, podremos derivar este trait para nuestro tipo personalizado, a menos que contenga arrays o diccionarios.

#[starknet::interface]
trait IStoringCustomType<TContractState> {
    fn set_person(ref self: TContractState, person: Person);
}

// Deriving the starknet::Store trait
// allows us to store the `Person` struct in the contract's storage.
#[derive(Drop, Serde, Copy, starknet::Store)]
struct Person {
    age: u8,
    name: felt252
}

#[starknet::contract]
mod StoringCustomType {
    use super::Person;

    #[storage]
    struct Storage {
        person: Person
    }

    #[abi(embed_v0)]
    impl StoringCustomType of super::IStoringCustomType<ContractState> {
        fn set_person(ref self: ContractState, person: Person) {
            self.person.write(person);
        }
    }
}

Juega con este contrato en Remix.

Last change: 2023-10-12, commit: 90aa7c0

Custom types in entrypoints

Using custom types in entrypoints requires our type to implement the Serde trait. This is because when calling an entrypoint, the input is sent as an array of felt252 to the entrypoint, and we need to be able to deserialize it into our custom type. Similarly, when returning a custom type from an entrypoint, we need to be able to serialize it into an array of felt252. Thankfully, we can just derive the Serde trait for our custom type.

#[starknet::interface]
trait ISerdeCustomType<TContractState> {
    fn person_input(ref self: TContractState, person: SerdeCustomType::Person);
    fn person_output(self: @TContractState) -> SerdeCustomType::Person;
}

#[starknet::contract]
mod SerdeCustomType {
    #[storage]
    struct Storage {}

    // Deriving the `Serde` trait allows us to use
    // the Person type as an entrypoint parameter and return value
    #[derive(Drop, Serde)]
    struct Person {
        age: u8,
        name: felt252
    }

    #[abi(embed_v0)]
    impl SerdeCustomType of super::ISerdeCustomType<ContractState> {
        fn person_input(ref self: ContractState, person: Person) {}

        fn person_output(self: @ContractState) -> Person {
            Person { age: 10, name: 'Joe' }
        }
    }
}

Juega con este contrato en Remix.

Last change: 2023-10-12, commit: 90aa7c0

Documentation

Es importante tomarse el tiempo para documentar su código. Ayudará a los desarrolladores y usuarios a comprender el contrato y sus funcionalidades.

En Cairo, puedes agregar comentarios con //.

Best Practices:

Since Cairo 1, the community has adopted a Rust-like documentation style.

Contract Interface:

In smart contracts, you will often have a trait that defines the contract's interface (with #[starknet::interface]). This is the perfect place to include detailed documentation explaining the purpose and functionality of the contract entry points. You can follow this template:

#[starknet::interface]
trait IContract<TContractState> {
    /// High-level description of the function
    ///
    /// # Arguments
    ///
    /// * `arg_1` - Description of the argument
    /// * `arg_n` - ...
    ///
    /// # Returns
    ///
    /// High-level description of the return value
    fn do_something(ref self: TContractState, arg_1: T_arg_1) -> T_return;
}

Tenga en cuenta que esto no debe describir los detalles de implementación de la función, sino más bien el propósito de high-level y la funcionalidad del contrato desde la perspectiva de un usuario.

Implementation Details:

Al escribir la lógica del contrato, puede agregar comentarios para describir los detalles técnicos de implementación de las funciones.

Avoid over-commenting: Comments should provide additional value and clarity.

Last change: 2023-12-05, commit: d8bdbed

Deploy and interact with contracts

En este capítulo, veremos cómo implementar e interactuar con contratos.

Last change: 2023-10-19, commit: dcadbd1

Contract interfaces and Traits generation

Las interfaces de contrato definen la estructura y el comportamiento de un contrato y sirven como la ABI pública del contrato. Enumeran todas las firmas de funciones que expone un contrato. Para obtener una explicación detallada de las interfaces, puede consultar el Cairo Book.

En Cairo, para especificar la interfaz es necesario definir un trait anotado con #[starknet::interface] y luego implementar ese trait en el contrato.

Cuando una función necesita acceder al estado del contrato, debe tener un parámetro self de tipo ContractState. Esto implica que la firma de función correspondiente en el trait de interfaz también debe tomar un tipo TContractState como parámetro. Es importante tener en cuenta que todas las funciones de la interfaz del contrato deben tener este parámetro self de tipo TContractState.

Puedes utilizar el atributo #[generate_trait] para generar implícitamente el trait para un bloque de implementación específico. Este atributo genera automáticamente un trait con las mismas funciones que las del bloque implementado, sustituyendo el parámetro self por un parámetro genérico TContractState. Sin embargo, tendrás que anotar el bloque con el atributo #[abi(per_item)], y cada función con el atributo apropiado dependiendo de si es una función externa, un constructor o un manejador l1.

En resumen, hay dos formas de manejar las interfaces:

  • Explicitly, by defining a trait annoted with #[starknet::interface]
  • Implicitly, by using #[generate_trait] combined with the #[abi(per_item)]` attributes, and annotating each function inside the implementation block with the appropriate attribute.

Explicit interface

#[starknet::interface]
trait IExplicitInterfaceContract<TContractState> {
    fn get_value(self: @TContractState) -> u32;
    fn set_value(ref self: TContractState, value: u32);
}

#[starknet::contract]
mod ExplicitInterfaceContract {
    #[storage]
    struct Storage {
        value: u32
    }

    #[abi(embed_v0)]
    impl ExplicitInterfaceContract of super::IExplicitInterfaceContract<ContractState> {
        fn get_value(self: @ContractState) -> u32 {
            self.value.read()
        }

        fn set_value(ref self: ContractState, value: u32) {
            self.value.write(value);
        }
    }
}

Juega con este contrato en Remix.

Implicit interface

#[starknet::contract]
mod ImplicitInterfaceContract {
    #[storage]
    struct Storage {
        value: u32
    }

    #[abi(per_item)]
    #[generate_trait]
    impl ImplicitInterfaceContract of IImplicitInterfaceContract {
        #[external(v0)]
        fn get_value(self: @ContractState) -> u32 {
            self.value.read()
        }

        #[external(v0)]
        fn set_value(ref self: ContractState, value: u32) {
            self.value.write(value);
        }
    }
}

Juega con este contrato en Remix.

Note: You can import an implicitly generated contract interface with use contract::{GeneratedContractInterface}. However, the Dispatcher will not be generated automatically.

Internal functions

You can also use #[generate_trait] for your internal functions. Since this trait is generated in the context of the contract, you can define pure functions as well (functions without the self parameter).

#[starknet::interface]
trait IImplicitInternalContract<TContractState> {
    fn add(ref self: TContractState, nb: u32);
    fn get_value(self: @TContractState) -> u32;
    fn get_const(self: @TContractState) -> u32;
}

#[starknet::contract]
mod ImplicitInternalContract {
    #[storage]
    struct Storage {
        value: u32
    }

    #[generate_trait]
    impl InternalFunctions of InternalFunctionsTrait {
        fn set_value(ref self: ContractState, value: u32) {
            self.value.write(value);
        }
        fn get_const() -> u32 {
            42
        }
    }

    #[constructor]
    fn constructor(ref self: ContractState) {
        self.set_value(0);
    }

    #[abi(embed_v0)]
    impl ImplicitInternalContract of super::IImplicitInternalContract<ContractState> {
        fn add(ref self: ContractState, nb: u32) {
            self.set_value(self.value.read() + nb);
        }
        fn get_value(self: @ContractState) -> u32 {
            self.value.read()
        }
        fn get_const(self: @ContractState) -> u32 {
            self.get_const()
        }
    }
}

Juega con este contrato en Remix.

Last change: 2023-11-26, commit: 35ec815

Calling other contracts

Hay dos formas diferentes de llamar a otros contratos en Cairo.

The easiest way to call other contracts is by using the dispatcher of the contract you want to call. You can read more about Dispatchers in the Cairo Book

La otra forma es utilizar la llamada al sistema starknet::call_contract_syscall. Sin embargo, este método no es recomendable.

Para llamar a otros contratos utilizando dispatchers, tendrás que definir la interfaz del contrato llamado como un trait anotado con el atributo #[starknet::interface], y luego importar los elementos IContractDispatcher e IContractDispatcherTrait en tu contrato.

#[starknet::interface]
trait ICallee<TContractState> {
    fn set_value(ref self: TContractState, value: u128) -> u128;
}

#[starknet::contract]
mod Callee {
    #[storage]
    struct Storage {
        value: u128,
    }

    #[abi(embed_v0)]
    impl ICalleeImpl of super::ICallee<ContractState> {
        fn set_value(ref self: ContractState, value: u128) -> u128 {
            self.value.write(value);
            value
        }
    }
}

Visita el contrato en Voyager o juega con él en Remix.

use starknet::ContractAddress;

// We need to have the interface of the callee contract defined
// so that we can import the Dispatcher.
#[starknet::interface]
trait ICallee<TContractState> {
    fn set_value(ref self: TContractState, value: u128) -> u128;
}

#[starknet::interface]
trait ICaller<TContractState> {
    fn set_value_from_address(ref self: TContractState, addr: ContractAddress, value: u128);
}

#[starknet::contract]
mod Caller {
    // We import the Dispatcher of the called contract
    use super::{ICalleeDispatcher, ICalleeDispatcherTrait};
    use starknet::ContractAddress;

    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl ICallerImpl of super::ICaller<ContractState> {
        fn set_value_from_address(ref self: ContractState, addr: ContractAddress, value: u128) {
            ICalleeDispatcher { contract_address: addr }.set_value(value);
        }
    }
}

Visita el contrato en Voyager o juega con él en Remix.

Last change: 2023-11-30, commit: fec6527

Factory Pattern

El patrón de factory es un patrón bien conocido en la programación orientada a objetos. Proporciona una abstracción sobre cómo instanciar una clase.

En el caso de los smart contracts, podemos utilizar este patrón definiendo un contrato de factory que tenga la responsabilidad exclusiva de crear y gestionar otros contratos.

Class hash and contract instance

En Starknet, hay una separación entre las clases de contrato y las instancias. Una clase de contrato sirve como plano, definido por el código de bytes subyacente de Cairo, los puntos de entrada del contrato, la ABI y el hash del programa Sierra. La clase del contrato es identificada por un hash de clase. Cuando se desea agregar una nueva clase a la red, primero es necesario declararla.

Cuando se despliega un contrato, es necesario especificar el hash de clase del contrato que se desea desplegar. Cada instancia de un contrato tiene su propio almacenamiento independientemente del hash de clase.

Utilizando el patrón de factory, podemos desplegar múltiples instancias de la misma clase de contrato y manejar las actualizaciones fácilmente.

Minimal example

He aquí un ejemplo mínimo de un contrato de factory que despliega el contrato SimpleCounter:

use starknet::{ContractAddress, ClassHash};

#[starknet::interface]
trait ICounterFactory<TContractState> {
    /// Create a new counter contract from stored arguments
    fn create_counter(ref self: TContractState) -> ContractAddress;

    /// Create a new counter contract from the given arguments
    fn create_counter_at(ref self: TContractState, init_value: u128) -> ContractAddress;

    /// Update the argument
    fn update_init_value(ref self: TContractState, init_value: u128);

    /// Update the class hash of the Counter contract to deploy when creating a new counter
    fn update_counter_class_hash(ref self: TContractState, counter_class_hash: ClassHash);
}

#[starknet::contract]
mod CounterFactory {
    use starknet::{ContractAddress, ClassHash};
    use starknet::syscalls::deploy_syscall;

    #[storage]
    struct Storage {
        /// Store the constructor arguments of the contract to deploy
        init_value: u128,
        /// Store the class hash of the contract to deploy
        counter_class_hash: ClassHash,
    }

    #[constructor]
    fn constructor(ref self: ContractState, init_value: u128, class_hash: ClassHash) {
        self.init_value.write(init_value);
        self.counter_class_hash.write(class_hash);
    }

    #[abi(embed_v0)]
    impl Factory of super::ICounterFactory<ContractState> {
        fn create_counter_at(ref self: ContractState, init_value: u128) -> ContractAddress {
            // Contructor arguments
            let mut constructor_calldata: Array::<felt252> = array![init_value.into()];

            // Contract deployment
            let (deployed_address, _) = deploy_syscall(
                self.counter_class_hash.read(), 0, constructor_calldata.span(), false
            )
                .expect('failed to deploy counter');

            deployed_address
        }

        fn create_counter(ref self: ContractState) -> ContractAddress {
            self.create_counter_at(self.init_value.read())
        }

        fn update_init_value(ref self: ContractState, init_value: u128) {
            self.init_value.write(init_value);
        }

        fn update_counter_class_hash(ref self: ContractState, counter_class_hash: ClassHash) {
            self.counter_class_hash.write(counter_class_hash);
        }
    }
}

Esta fábrica se puede utilizar para desplegar múltiples instancias del contrato SimpleCounter llamando a las funciones create_counter y create_counter_at.

El hash de la clase SimpleCounter se almacena dentro de la fábrica, y se puede actualizar con la función update_counter_class_hash que permite reutilizar el mismo contrato de fábrica cuando se actualiza el contrato SimpleCounter.

Este ejemplo mínimo carece de varias funciones útiles, como el control de acceso, el seguimiento de los contratos desplegados, los eventos, ...

Last change: 2023-10-19, commit: dcadbd1

Contract Testing

Las pruebas juegan un papel crucial en el desarrollo de software, especialmente para los smart contracts. En esta sección, te guiaremos a través de los conceptos básicos de las pruebas de un smart contracts en Starknet con scarb.

Empecemos con un simple contrato inteligente como ejemplo:

use starknet::ContractAddress;

#[starknet::interface]
trait ISimpleContract<TContractState> {
    fn get_value(self: @TContractState) -> u32;
    fn get_owner(self: @TContractState) -> ContractAddress;
    fn set_value(ref self: TContractState, value: u32);
}

#[starknet::contract]
mod SimpleContract {
    use starknet::{get_caller_address, ContractAddress};

    #[storage]
    struct Storage {
        value: u32,
        owner: ContractAddress
    }

    #[constructor]
    fn constructor(ref self: ContractState, initial_value: u32) {
        self.value.write(initial_value);
        self.owner.write(get_caller_address());
    }

    #[abi(embed_v0)]
    impl SimpleContract of super::ISimpleContract<ContractState> {
        fn get_value(self: @ContractState) -> u32 {
            self.value.read()
        }

        fn get_owner(self: @ContractState) -> ContractAddress {
            self.owner.read()
        }

        fn set_value(ref self: ContractState, value: u32) {
            assert(self.owner.read() == get_caller_address(), 'Not owner');
            self.value.write(value);
        }
    }
}

Ahora, eche un vistazo a las pruebas de este contrato:

#[cfg(test)]
mod tests {
    // Import the interface and dispatcher to be able to interact with the contract.
    use testing_how_to::contract::{
        ISimpleContract, SimpleContract, ISimpleContractDispatcher, ISimpleContractDispatcherTrait
    };

    // Import the deploy syscall to be able to deploy the contract.
    use starknet::class_hash::Felt252TryIntoClassHash;
    use starknet::{
        deploy_syscall, ContractAddress, get_caller_address, get_contract_address,
        contract_address_const
    };

    // Use starknet test utils to fake the transaction context.
    use starknet::testing::{set_caller_address, set_contract_address};

    // Deploy the contract and return its dispatcher.
    fn deploy(initial_value: u32) -> ISimpleContractDispatcher {
        // Set up constructor arguments.
        let mut calldata = ArrayTrait::new();
        initial_value.serialize(ref calldata);

        // Declare and deploy
        let (contract_address, _) = deploy_syscall(
            SimpleContract::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false
        )
            .unwrap();

        // Return the dispatcher.
        // The dispatcher allows to interact with the contract based on its interface.
        ISimpleContractDispatcher { contract_address }
    }

    #[test]
    #[available_gas(2000000000)]
    fn test_deploy() {
        let initial_value: u32 = 10;
        let contract = deploy(initial_value);

        assert(contract.get_value() == initial_value, 'wrong initial value');
        assert(contract.get_owner() == get_contract_address(), 'wrong owner');
    }

    #[test]
    #[available_gas(2000000000)]
    fn test_set_as_owner() {
        // Fake the caller address to address 1
        let owner = contract_address_const::<1>();
        set_contract_address(owner);

        let contract = deploy(10);
        assert(contract.get_owner() == owner, 'wrong owner');

        // Fake the contract address to address 1
        set_contract_address(owner);
        let new_value: u32 = 20;
        contract.set_value(new_value);

        assert(contract.get_value() == new_value, 'wrong value');
    }

    #[test]
    #[should_panic]
    #[available_gas(2000000000)]
    fn test_set_not_owner() {
        let owner = contract_address_const::<1>();
        set_contract_address(owner);

        let contract = deploy(10);

        let not_owner = contract_address_const::<2>();
        set_contract_address(not_owner);

        let new_value: u32 = 20;
        contract.set_value(new_value);
    }
}

Play with this contract in Remix.

Para definir nuestro test, usamos scarb, que nos permite crear un módulo separado protegido con #[cfg(test)]. Esto asegura que el módulo de prueba sólo se compila cuando se ejecutan pruebas utilizando scarb test.

Cada prueba se define como una función con el atributo #[test]. También puede comprobar si una prueba entra en pánico utilizando el atributo #[should_panic].

Como estamos en el contexto de un smart contract, es esencial establecer el límite de gas. Esto se hace utilizando el atributo #[available_gas(X)] para especificar el límite de gas para una prueba. ¡Esta es también una gran manera de asegurar que las funciones de tu contrato se mantienen por debajo de un cierto límite de gas!

Note: The term "gas" here refers to Sierra gas, not L1 gas

Pasemos ahora al proceso de prueba:

  • Use the deploy function logic to declare and deploy your contract.
  • Use assert to verify that the contract behaves as expected in the given context.

Para que las pruebas resulten más cómodas, el módulo testing de corelib proporciona algunas funciones útiles:

  • set_caller_address(address: ContractAddress)
  • set_contract_address(address: ContractAddress)
  • set_block_number(block_number: u64)
  • set_block_timestamp(block_timestamp: u64)
  • set_account_contract_address(address: ContractAddress)
  • set_max_fee(fee: u128)

También puede necesitar el módulo info de corelib, que le permite acceder a información sobre el contexto de la transacción actual:

  • get_caller_address() -> ContractAddress
  • get_contract_address() -> ContractAddress
  • get_block_info() -> Box<BlockInfo>
  • get_tx_info() -> Box<TxInfo>
  • get_block_timestamp() -> u64
  • get_block_number() -> u64

You can found the full list of functions in the Starknet Corelib repo. You can also find a detailled explaination of testing in cairo in the Cairo book - Chapter 8.

Starknet Foundry

Starknet Foundry es un potente conjunto de herramientas para el desarrollo de contratos inteligentes en Starknet. Ofrece soporte para probar contratos inteligentes Starknet sobre scarb con la herramienta Forge.

Probar con snforge es similar al proceso que acabamos de describir, pero simplificado. Además, hay características adicionales en camino, incluyendo cheatcodes o ejecución de pruebas en paralelo. Recomendamos encarecidamente explorar Starknet Foundry e incorporarlo a tus proyectos.

Si desea información más detallada sobre los contratos de pruebas con Starknet Foundry, consulte el Starknet Foundry Book - Testing Contracts.

Last change: 2023-12-06, commit: 1af1816

Cairo Cheatsheet

Este capítulo pretende ofrecer una referencia rápida para las construcciones más comunes de Cairo.

Last change: 2023-10-31, commit: d530b71

Felt252

Felt252 is a fundamental data type in Cairo from which all other data types are derived. Felt252 can also be used to store short-string representations with a maximum length of 31 characters.

Por ejemplo:

    let felt: felt252 = 100;
    let felt_as_str = 'Hello Starknet!';

    let felt = felt + felt_as_str;
Last change: 2023-11-30, commit: 932307f

Mapping

The LegacyMap type can be used to represent a collection of key-value.

use starknet::ContractAddress;

#[starknet::interface]
trait IMappingExample<TContractState> {
    fn register_user(ref self: TContractState, student_add: ContractAddress, studentName: felt252);
    fn record_student_score(
        ref self: TContractState, student_add: ContractAddress, subject: felt252, score: u16
    );
    fn view_student_name(self: @TContractState, student_add: ContractAddress) -> felt252;
    fn view_student_score(
        self: @TContractState, student_add: ContractAddress, subject: felt252
    ) -> u16;
}

#[starknet::contract]
mod MappingContract {
    use starknet::ContractAddress;

    #[storage]
    struct Storage {
        students_name: LegacyMap::<ContractAddress, felt252>,
        students_result_record: LegacyMap::<(ContractAddress, felt252), u16>,
    }

    #[abi(embed_v0)]
    impl External of super::IMappingExample<ContractState> {
        fn register_user(
            ref self: ContractState, student_add: ContractAddress, studentName: felt252
        ) {
            self.students_name.write(student_add, studentName);
        }

        fn record_student_score(
            ref self: ContractState, student_add: ContractAddress, subject: felt252, score: u16
        ) {
            self.students_result_record.write((student_add, subject), score);
        }

        fn view_student_name(self: @ContractState, student_add: ContractAddress) -> felt252 {
            self.students_name.read(student_add)
        }

        fn view_student_score(
            self: @ContractState, student_add: ContractAddress, subject: felt252
        ) -> u16 {
            // for a 2D mapping its important to take note of the amount of brackets being used.
            self.students_result_record.read((student_add, subject))
        }
    }
}
Last change: 2023-10-31, commit: 0d79abb

Arrays

Arrays are collections of elements of the same type. The possible operations on arrays are defined with the array::ArrayTrait of the corelib:

trait ArrayTrait<T> {
    fn new() -> Array<T>;
    fn append(ref self: Array<T>, value: T);
    fn pop_front(ref self: Array<T>) -> Option<T> nopanic;
    fn pop_front_consume(self: Array<T>) -> Option<(Array<T>, T)> nopanic;
    fn get(self: @Array<T>, index: usize) -> Option<Box<@T>>;
    fn at(self: @Array<T>, index: usize) -> @T;
    fn len(self: @Array<T>) -> usize;
    fn is_empty(self: @Array<T>) -> bool;
    fn span(self: @Array<T>) -> Span<T>;
}

Por ejemplo:

fn array() -> bool {
    let mut arr = ArrayTrait::<u32>::new();
    arr.append(10);
    arr.append(20);
    arr.append(30);

    assert(arr.len() == 3, 'array length should be 3');

    let first_value = arr.pop_front().unwrap();
    assert(first_value == 10, 'first value should match');

    let second_value = *arr.at(0);
    assert(second_value == 20, 'second value should match');

    // Returns true if an array is empty, then false if it isn't.
    arr.is_empty()
}
Last change: 2023-10-31, commit: 0d79abb

Loop

A loop specifies a block of code that will run repetitively until a halting condition is encountered. For example:

    let mut arr = ArrayTrait::new();

    // Same as ~ while (i < 10) arr.append(i++);
    let mut i: u32 = 0;
    let limit = 10;
    loop {
        if i == limit {
            break;
        };

        arr.append(i);

        i += 1;
    };
Last change: 2023-11-30, commit: 932307f

Match

The "match" expression in Cairo allows us to control the flow of our code by comparing a felt data type or an enum against various patterns and then running specific code based on the pattern that matches. For example:

#[derive(Drop, Serde)]
enum Colour {
    Red,
    Blue,
    Green,
    Orange,
    Black
}

#[derive(Drop, Serde)]
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> felt252 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

fn specified_colour(colour: Colour) -> felt252 {
    let mut response: felt252 = '';

    match colour {
        Colour::Red => { response = 'You passed in Red'; },
        Colour::Blue => { response = 'You passed in Blue'; },
        Colour::Green => { response = 'You passed in Green'; },
        Colour::Orange => { response = 'You passed in Orange'; },
        Colour::Black => { response = 'You passed in Black'; },
    };

    response
}

fn quiz(num: felt252) -> felt252 {
    let mut response: felt252 = '';

    match num {
        0 => { response = 'You failed' },
        _ => { response = 'You Passed' },
    };

    response
}
Last change: 2023-10-31, commit: 0d79abb

Tuples

Tuples is a data type to group a fixed number of items of potentially different types into a single compound structure. Unlike arrays, tuples have a set length and can contain elements of varying types. Once a tuple is created, its size cannot change. For example:

    let address = "0x000";
    let age = 20;
    let active = true;

    // Create tuple
    let user_tuple = (address, age, active);

    // Access tuple
    let (address, age, active) = stored_tuple;
Last change: 2023-11-30, commit: 932307f

Struct

A struct is a data type similar to tuple. Like tuples they can be used to hold data of different types. For example:

// With Store, you can store Data's structs in the storage part of contracts.
#[derive(Drop, starknet::Store)]
struct Data {
    address: starknet::ContractAddress,
    age: u8
}
Last change: 2023-10-31, commit: 0d79abb

Type casting

Cairo supports the conversion from one scalar types to another by using the into and try_into methods. traits::Into is used for conversion from a smaller data type to a larger data type, while traits::TryInto is used when converting from a larger to a smaller type that might not fit. For example:

    let a_number: u32 = 15;
    let my_felt252 = 15;

    // Since a u32 might not fit in a u8 and a u16, we need to use try_into,
    // then unwrap the Option<T> type thats returned.
    let new_u8: u8 = a_number.try_into().unwrap();
    let new_u16: u16 = a_number.try_into().unwrap();

    // since new_u32 is the of the same type (u32) as rand_number, we can directly assign them,
    // or use the .into() method.
    let new_u32: u32 = a_number;

    // When typecasting from a smaller size to an equal or larger size we use the .into() method.
    // Note: u64 and u128 are larger than u32, so a u32 type will always fit into them.
    let new_u64: u64 = a_number.into();
    let new_u128: u128 = a_number.into();

    // Since a felt252 is smaller than a u256, we can use the into() method
    let new_u256: u256 = my_felt252.into();
    let new_felt252: felt252 = new_u16.into();

    //note a usize is smaller than a felt so we use the try_into
    let new_usize: usize = my_felt252.try_into().unwrap();
Last change: 2023-11-30, commit: 932307f

Upgradeable Contract

In Starknet, contracts are divided into two parts: contract classes and contract instances. This division follows a similar concept used in object-oriented programming languages, where we distinguish between the definition and implementation of objects.

A contract class is the definition of a contract: it specifies how the contract behaves. It contains essential information like the Cairo byte code, hint information, entry point names, and everything that defines its semantics unambiguously.

To identify different contract classes, Starknet assigns a unique identifier to each class: the class hash. A contract instance is a deployed contract that corresponds to a specific contract class. Think of it as an instance of an object in languages like Java.

Cada clase se identifica por su hash de clase, que es análogo a un nombre de clase en un lenguaje de programación orientado a objetos. Una instancia de contrato es un contrato desplegado correspondiente a una clase.

Puedes actualizar un contrato desplegado a una versión más reciente llamando a la función replace_class_syscall. Usando esta función, puedes actualizar el hash de clase asociado con un contrato desplegado, actualizando efectivamente su implementación. Sin embargo, esto no modificará el almacenamiento del contrato, por lo que todos los datos almacenados en el contrato seguirán siendo los mismos.

To illustrate this concept, let's consider an example with two contracts: UpgradeableContract_V0, and UpgradeableContract_V1. Start by deploying UpgradeableContract_V0 as the initial version. Next, send a transaction that invokes the upgrade function, with the class hash of UpgradeableContract_V1 as parameter to upgrade the class hash of the deployed contract to the UpgradeableContract_V1 one. Then, call the version method on the contract to see that the contract was upgraded to the V1 version.

use starknet::class_hash::ClassHash;

#[starknet::interface]
trait IUpgradeableContract<TContractState> {
    fn upgrade(ref self: TContractState, impl_hash: ClassHash);
    fn version(self: @TContractState) -> u8;
}

#[starknet::contract]
mod UpgradeableContract_V0 {
    use starknet::class_hash::ClassHash;
    use starknet::SyscallResultTrait;

    #[storage]
    struct Storage {}


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

    #[derive(Drop, starknet::Event)]
    struct Upgraded {
        implementation: ClassHash
    }

    #[abi(embed_v0)]
    impl UpgradeableContract of super::IUpgradeableContract<ContractState> {
        fn upgrade(ref self: ContractState, impl_hash: ClassHash) {
            assert(!impl_hash.is_zero(), 'Class hash cannot be zero');
            starknet::replace_class_syscall(impl_hash).unwrap_syscall();
            self.emit(Event::Upgraded(Upgraded { implementation: impl_hash }))
        }

        fn version(self: @ContractState) -> u8 {
            0
        }
    }
}

Visita el contrato en Voyager o juega con él en Remix.

use starknet::class_hash::ClassHash;

#[starknet::interface]
trait IUpgradeableContract<TContractState> {
    fn upgrade(ref self: TContractState, impl_hash: ClassHash);
    fn version(self: @TContractState) -> u8;
}

#[starknet::contract]
mod UpgradeableContract_V1 {
    use starknet::class_hash::ClassHash;
    use starknet::SyscallResultTrait;

    #[storage]
    struct Storage {}


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

    #[derive(Drop, starknet::Event)]
    struct Upgraded {
        implementation: ClassHash
    }

    #[abi(embed_v0)]
    impl UpgradeableContract of super::IUpgradeableContract<ContractState> {
        fn upgrade(ref self: ContractState, impl_hash: ClassHash) {
            assert(!impl_hash.is_zero(), 'Class hash cannot be zero');
            starknet::replace_class_syscall(impl_hash).unwrap_syscall();
            self.emit(Event::Upgraded(Upgraded { implementation: impl_hash }))
        }

        fn version(self: @ContractState) -> u8 {
            1
        }
    }
}

Visita el contrato en Voyager o juega con él en Remix.

Last change: 2023-10-12, commit: 90aa7c0

Simple Defi Vault

This is the Cairo adaptation of the Solidity by example Vault. Here's how it works:

  • When a user deposits a token, the contract calculates the amount of shares to mint.

  • When a user withdraws, the contract burns their shares, calculates the yield, and withdraw both the yield and the initial amount of token deposited.

use starknet::{ContractAddress};

// In order to make contract calls within our Vault,
// we need to have the interface of the remote ERC20 contract defined to import the Dispatcher.
#[starknet::interface]
trait IERC20<TContractState> {
    fn name(self: @TContractState) -> felt252;
    fn symbol(self: @TContractState) -> felt252;
    fn decimals(self: @TContractState) -> u8;
    fn total_supply(self: @TContractState) -> u256;
    fn balance_of(self: @TContractState, account: ContractAddress) -> u256;
    fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256;
    fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
    fn transfer_from(
        ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256
    ) -> bool;
    fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool;
}

#[starknet::interface]
trait ISimpleVault<TContractState> {
    fn deposit(ref self: TContractState, amount: u256);
    fn withdraw(ref self: TContractState, shares: u256);
}

#[starknet::contract]
mod SimpleVault {
    use super::{IERC20Dispatcher, IERC20DispatcherTrait};
    use starknet::{ContractAddress, get_caller_address, get_contract_address};
    #[storage]
    struct Storage {
        token: IERC20Dispatcher,
        total_supply: u256,
        balance_of: LegacyMap<ContractAddress, u256>
    }

    #[constructor]
    fn constructor(ref self: ContractState, token: ContractAddress) {
        self.token.write(IERC20Dispatcher { contract_address: token });
    }

    #[generate_trait]
    impl PrivateFunctions of PrivateFunctionsTrait {
        fn _mint(ref self: ContractState, to: ContractAddress, shares: u256) {
            self.total_supply.write(self.total_supply.read() + shares);
            self.balance_of.write(to, self.balance_of.read(to) + shares);
        }

        fn _burn(ref self: ContractState, from: ContractAddress, shares: u256) {
            self.total_supply.write(self.total_supply.read() - shares);
            self.balance_of.write(from, self.balance_of.read(from) - shares);
        }
    }

    #[abi(embed_v0)]
    impl SimpleVault of super::ISimpleVault<ContractState> {
        fn deposit(ref self: ContractState, amount: u256) {
            // a = amount
            // B = balance of token before deposit
            // T = total supply
            // s = shares to mint
            //
            // (T + s) / T = (a + B) / B 
            //
            // s = aT / B
            let caller = get_caller_address();
            let this = get_contract_address();

            let mut shares = 0;
            if self.total_supply.read() == 0 {
                shares = amount;
            } else {
                let balance = self.token.read().balance_of(this);
                shares = (amount * self.total_supply.read()) / balance;
            }

            PrivateFunctions::_mint(ref self, caller, shares);
            self.token.read().transfer_from(caller, this, amount);
        }

        fn withdraw(ref self: ContractState, shares: u256) {
            // a = amount
            // B = balance of token before withdraw
            // T = total supply
            // s = shares to burn
            //
            // (T - s) / T = (B - a) / B 
            //
            // a = sB / T
            let caller = get_caller_address();
            let this = get_contract_address();

            let balance = self.token.read().balance_of(this);
            let amount = (shares * balance) / self.total_supply.read();
            PrivateFunctions::_burn(ref self, caller, shares);
            self.token.read().transfer(caller, amount);
        }
    }
}

Juega con este contrato en Remix.

Last change: 2023-10-12, commit: 90aa7c0

ERC20 Token

Los contratos que siguen el Standard ERC20 se denominan tokens ERC20. Se utilizan para representar activos fungibles.

Para crear un contrato ERC20, debe implementar la siguiente interfaz:

#[starknet::interface]
trait IERC20<TContractState> {
    fn get_name(self: @TContractState) -> felt252;
    fn get_symbol(self: @TContractState) -> felt252;
    fn get_decimals(self: @TContractState) -> u8;
    fn get_total_supply(self: @TContractState) -> felt252;
    fn balance_of(self: @TContractState, account: ContractAddress) -> felt252;
    fn allowance(
        self: @TContractState, owner: ContractAddress, spender: ContractAddress
    ) -> felt252;
    fn transfer(ref self: TContractState, recipient: ContractAddress, amount: felt252);
    fn transfer_from(
        ref self: TContractState,
        sender: ContractAddress,
        recipient: ContractAddress,
        amount: felt252
    );
    fn approve(ref self: TContractState, spender: ContractAddress, amount: felt252);
    fn increase_allowance(ref self: TContractState, spender: ContractAddress, added_value: felt252);
    fn decrease_allowance(
        ref self: TContractState, spender: ContractAddress, subtracted_value: felt252
    );
}

In Starknet, function names should be written in snake_case. This is not the case in Solidity, where function names are written in camelCase. The Starknet ERC20 interface is therefore slightly different from the Solidity ERC20 interface.

He aquí una implementación de la interfaz ERC20 en Cairo:

#[starknet::contract]
mod erc20 {
    use zeroable::Zeroable;
    use starknet::get_caller_address;
    use starknet::contract_address_const;
    use starknet::ContractAddress;

    #[storage]
    struct Storage {
        name: felt252,
        symbol: felt252,
        decimals: u8,
        total_supply: felt252,
        balances: LegacyMap::<ContractAddress, felt252>,
        allowances: LegacyMap::<(ContractAddress, ContractAddress), felt252>,
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        Transfer: Transfer,
        Approval: Approval,
    }
    #[derive(Drop, starknet::Event)]
    struct Transfer {
        from: ContractAddress,
        to: ContractAddress,
        value: felt252,
    }
    #[derive(Drop, starknet::Event)]
    struct Approval {
        owner: ContractAddress,
        spender: ContractAddress,
        value: felt252,
    }

    mod Errors {
        const APPROVE_FROM_ZERO: felt252 = 'ERC20: approve from 0';
        const APPROVE_TO_ZERO: felt252 = 'ERC20: approve to 0';
        const TRANSFER_FROM_ZERO: felt252 = 'ERC20: transfer from 0';
        const TRANSFER_TO_ZERO: felt252 = 'ERC20: transfer to 0';
        const BURN_FROM_ZERO: felt252 = 'ERC20: burn from 0';
        const MINT_TO_ZERO: felt252 = 'ERC20: mint to 0';
    }

    #[constructor]
    fn constructor(
        ref self: ContractState,
        recipient: ContractAddress,
        name: felt252,
        decimals: u8,
        initial_supply: felt252,
        symbol: felt252
    ) {
        self.name.write(name);
        self.symbol.write(symbol);
        self.decimals.write(decimals);
        self.mint(recipient, initial_supply);
    }

    #[abi(embed_v0)]
    impl IERC20Impl of super::IERC20<ContractState> {
        fn get_name(self: @ContractState) -> felt252 {
            self.name.read()
        }

        fn get_symbol(self: @ContractState) -> felt252 {
            self.symbol.read()
        }

        fn get_decimals(self: @ContractState) -> u8 {
            self.decimals.read()
        }

        fn get_total_supply(self: @ContractState) -> felt252 {
            self.total_supply.read()
        }

        fn balance_of(self: @ContractState, account: ContractAddress) -> felt252 {
            self.balances.read(account)
        }

        fn allowance(
            self: @ContractState, owner: ContractAddress, spender: ContractAddress
        ) -> felt252 {
            self.allowances.read((owner, spender))
        }

        fn transfer(ref self: ContractState, recipient: ContractAddress, amount: felt252) {
            let sender = get_caller_address();
            self._transfer(sender, recipient, amount);
        }

        fn transfer_from(
            ref self: ContractState,
            sender: ContractAddress,
            recipient: ContractAddress,
            amount: felt252
        ) {
            let caller = get_caller_address();
            self.spend_allowance(sender, caller, amount);
            self._transfer(sender, recipient, amount);
        }

        fn approve(ref self: ContractState, spender: ContractAddress, amount: felt252) {
            let caller = get_caller_address();
            self.approve_helper(caller, spender, amount);
        }

        fn increase_allowance(
            ref self: ContractState, spender: ContractAddress, added_value: felt252
        ) {
            let caller = get_caller_address();
            self
                .approve_helper(
                    caller, spender, self.allowances.read((caller, spender)) + added_value
                );
        }

        fn decrease_allowance(
            ref self: ContractState, spender: ContractAddress, subtracted_value: felt252
        ) {
            let caller = get_caller_address();
            self
                .approve_helper(
                    caller, spender, self.allowances.read((caller, spender)) - subtracted_value
                );
        }
    }

    #[generate_trait]
    impl InternalImpl of InternalTrait {
        fn _transfer(
            ref self: ContractState,
            sender: ContractAddress,
            recipient: ContractAddress,
            amount: felt252
        ) {
            assert(!sender.is_zero(), Errors::TRANSFER_FROM_ZERO);
            assert(!recipient.is_zero(), Errors::TRANSFER_TO_ZERO);
            self.balances.write(sender, self.balances.read(sender) - amount);
            self.balances.write(recipient, self.balances.read(recipient) + amount);
            self.emit(Transfer { from: sender, to: recipient, value: amount });
        }

        fn spend_allowance(
            ref self: ContractState,
            owner: ContractAddress,
            spender: ContractAddress,
            amount: felt252
        ) {
            let allowance = self.allowances.read((owner, spender));
            self.allowances.write((owner, spender), allowance - amount);
        }

        fn approve_helper(
            ref self: ContractState,
            owner: ContractAddress,
            spender: ContractAddress,
            amount: felt252
        ) {
            assert(!spender.is_zero(), Errors::APPROVE_TO_ZERO);
            self.allowances.write((owner, spender), amount);
            self.emit(Approval { owner, spender, value: amount });
        }

        fn mint(ref self: ContractState, recipient: ContractAddress, amount: felt252) {
            assert(!recipient.is_zero(), Errors::MINT_TO_ZERO);
            let supply = self.total_supply.read() + amount; // What can go wrong here?
            self.total_supply.write(supply);
            let balance = self.balances.read(recipient) + amount;
            self.balances.write(recipient, amount);
            self
                .emit(
                    Event::Transfer(
                        Transfer {
                            from: contract_address_const::<0>(), to: recipient, value: amount
                        }
                    )
                );
        }
    }
}

Juega con este contrato en Remix.

Existen otras implementaciones, como la Open Zeppelin o la Cairo By Example.

Last change: 2023-10-24, commit: c372633

Constant Product AMM

Se trata de la adaptación cairota de la Solidez por ejemplo Producto Constante AMM.

use starknet::ContractAddress;

#[starknet::interface]
trait IConstantProductAmm<TContractState> {
    fn swap(ref self: TContractState, token_in: ContractAddress, amount_in: u256) -> u256;
    fn add_liquidity(ref self: TContractState, amount0: u256, amount1: u256) -> u256;
    fn remove_liquidity(ref self: TContractState, shares: u256) -> (u256, u256);
}

#[starknet::contract]
mod ConstantProductAmm {
    use core::traits::Into;
    use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
    use starknet::{
        ContractAddress, get_caller_address, get_contract_address, contract_address_const
    };
    use integer::u256_sqrt;

    #[storage]
    struct Storage {
        token0: IERC20Dispatcher,
        token1: IERC20Dispatcher,
        reserve0: u256,
        reserve1: u256,
        total_supply: u256,
        balance_of: LegacyMap::<ContractAddress, u256>,
        // Fee 0 - 1000 (0% - 100%, 1 decimal places)
        // E.g. 3 = 0.3%
        fee: u16,
    }

    #[constructor]
    fn constructor(
        ref self: ContractState, token0: ContractAddress, token1: ContractAddress, fee: u16
    ) {
        // assert(fee <= 1000, 'fee > 1000');
        self.token0.write(IERC20Dispatcher { contract_address: token0 });
        self.token1.write(IERC20Dispatcher { contract_address: token1 });
        self.fee.write(fee);
    }

    #[generate_trait]
    impl PrivateFunctions of PrivateFunctionsTrait {
        fn _mint(ref self: ContractState, to: ContractAddress, amount: u256) {
            self.balance_of.write(to, self.balance_of.read(to) + amount);
            self.total_supply.write(self.total_supply.read() + amount);
        }

        fn _burn(ref self: ContractState, from: ContractAddress, amount: u256) {
            self.balance_of.write(from, self.balance_of.read(from) - amount);
            self.total_supply.write(self.total_supply.read() - amount);
        }

        fn _update(ref self: ContractState, reserve0: u256, reserve1: u256) {
            self.reserve0.write(reserve0);
            self.reserve1.write(reserve1);
        }

        #[inline(always)]
        fn select_token(self: @ContractState, token: ContractAddress) -> bool {
            assert(
                token == self.token0.read().contract_address
                    || token == self.token1.read().contract_address,
                'invalid token'
            );
            token == self.token0.read().contract_address
        }

        #[inline(always)]
        fn min(x: u256, y: u256) -> u256 {
            if (x <= y) {
                x
            } else {
                y
            }
        }
    }

    #[abi(embed_v0)]
    impl ConstantProductAmm of super::IConstantProductAmm<ContractState> {
        fn swap(ref self: ContractState, token_in: ContractAddress, amount_in: u256) -> u256 {
            assert(amount_in > 0, 'amount in = 0');
            let is_token0: bool = self.select_token(token_in);

            let (token0, token1): (IERC20Dispatcher, IERC20Dispatcher) = (
                self.token0.read(), self.token1.read()
            );
            let (reserve0, reserve1): (u256, u256) = (self.reserve0.read(), self.reserve1.read());
            let (
                token_in, token_out, reserve_in, reserve_out
            ): (IERC20Dispatcher, IERC20Dispatcher, u256, u256) =
                if (is_token0) {
                (token0, token1, reserve0, reserve1)
            } else {
                (token1, token0, reserve1, reserve0)
            };

            let caller = get_caller_address();
            let this = get_contract_address();
            token_in.transfer_from(caller, this, amount_in);

            // How much dy for dx?
            // xy = k
            // (x + dx)(y - dy) = k
            // y - dy = k / (x + dx)
            // y - k / (x + dx) = dy
            // y - xy / (x + dx) = dy
            // (yx + ydx - xy) / (x + dx) = dy
            // ydx / (x + dx) = dy

            let amount_in_with_fee = (amount_in * (1000 - self.fee.read().into()) / 1000);
            let amount_out = (reserve_out * amount_in_with_fee) / (reserve_in + amount_in_with_fee);

            token_out.transfer(caller, amount_out);

            self._update(self.token0.read().balance_of(this), self.token1.read().balance_of(this));
            amount_out
        }

        fn add_liquidity(ref self: ContractState, amount0: u256, amount1: u256) -> u256 {
            let caller = get_caller_address();
            let this = get_contract_address();
            let (token0, token1): (IERC20Dispatcher, IERC20Dispatcher) = (
                self.token0.read(), self.token1.read()
            );

            token0.transfer_from(caller, this, amount0);
            token1.transfer_from(caller, this, amount1);

            // How much dx, dy to add?
            //
            // xy = k
            // (x + dx)(y + dy) = k'
            //
            // No price change, before and after adding liquidity
            // x / y = (x + dx) / (y + dy)
            //
            // x(y + dy) = y(x + dx)
            // x * dy = y * dx
            //
            // x / y = dx / dy
            // dy = y / x * dx

            let (reserve0, reserve1): (u256, u256) = (self.reserve0.read(), self.reserve1.read());
            if (reserve0 > 0 || reserve1 > 0) {
                assert(reserve0 * amount1 == reserve1 * amount0, 'x / y != dx / dy');
            }

            // How much shares to mint?
            //
            // f(x, y) = value of liquidity
            // We will define f(x, y) = sqrt(xy)
            //
            // L0 = f(x, y)
            // L1 = f(x + dx, y + dy)
            // T = total shares
            // s = shares to mint
            //
            // Total shares should increase proportional to increase in liquidity
            // L1 / L0 = (T + s) / T
            //
            // L1 * T = L0 * (T + s)
            //
            // (L1 - L0) * T / L0 = s

            // Claim
            // (L1 - L0) / L0 = dx / x = dy / y
            //
            // Proof
            // --- Equation 1 ---
            // (L1 - L0) / L0 = (sqrt((x + dx)(y + dy)) - sqrt(xy)) / sqrt(xy)
            //
            // dx / dy = x / y so replace dy = dx * y / x
            //
            // --- Equation 2 ---
            // Equation 1 = (sqrt(xy + 2ydx + dx^2 * y / x) - sqrt(xy)) / sqrt(xy)
            //
            // Multiply by sqrt(x) / sqrt(x)
            // Equation 2 = (sqrt(x^2y + 2xydx + dx^2 * y) - sqrt(x^2y)) / sqrt(x^2y)
            //            = (sqrt(y)(sqrt(x^2 + 2xdx + dx^2) - sqrt(x^2)) / (sqrt(y)sqrt(x^2))
            // sqrt(y) on top and bottom cancels out
            //
            // --- Equation 3 ---
            // Equation 2 = (sqrt(x^2 + 2xdx + dx^2) - sqrt(x^2)) / (sqrt(x^2)
            // = (sqrt((x + dx)^2) - sqrt(x^2)) / sqrt(x^2)
            // = ((x + dx) - x) / x
            // = dx / x
            // Since dx / dy = x / y,
            // dx / x = dy / y
            //
            // Finally
            // (L1 - L0) / L0 = dx / x = dy / y

            let total_supply = self.total_supply.read();
            let shares = if (total_supply == 0) {
                u256_sqrt(amount0 * amount1).into()
            } else {
                PrivateFunctions::min(
                    amount0 * total_supply / reserve0, amount1 * total_supply / reserve1
                )
            };
            assert(shares > 0, 'shares = 0');
            self._mint(caller, shares);

            self._update(self.token0.read().balance_of(this), self.token1.read().balance_of(this));
            shares
        }

        fn remove_liquidity(ref self: ContractState, shares: u256) -> (u256, u256) {
            let caller = get_caller_address();
            let this = get_contract_address();
            let (token0, token1): (IERC20Dispatcher, IERC20Dispatcher) = (
                self.token0.read(), self.token1.read()
            );

            // Claim
            // dx, dy = amount of liquidity to remove
            // dx = s / T * x
            // dy = s / T * y
            //
            // Proof
            // Let's find dx, dy such that
            // v / L = s / T
            //
            // where
            // v = f(dx, dy) = sqrt(dxdy)
            // L = total liquidity = sqrt(xy)
            // s = shares
            // T = total supply
            //
            // --- Equation 1 ---
            // v = s / T * L
            // sqrt(dxdy) = s / T * sqrt(xy)
            //
            // Amount of liquidity to remove must not change price so
            // dx / dy = x / y
            //
            // replace dy = dx * y / x
            // sqrt(dxdy) = sqrt(dx * dx * y / x) = dx * sqrt(y / x)
            //
            // Divide both sides of Equation 1 with sqrt(y / x)
            // dx = s / T * sqrt(xy) / sqrt(y / x)
            // = s / T * sqrt(x^2) = s / T * x
            //
            // Likewise
            // dy = s / T * y

            // bal0 >= reserve0
            // bal1 >= reserve1
            let (bal0, bal1): (u256, u256) = (token0.balance_of(this), token1.balance_of(this));

            let total_supply = self.total_supply.read();
            let (amount0, amount1): (u256, u256) = (
                (shares * bal0) / total_supply, (shares * bal1) / total_supply
            );
            assert(amount0 > 0 && amount1 > 0, 'amount0 or amount1 = 0');

            self._burn(caller, shares);
            self._update(bal0 - amount0, bal1 - amount1);

            token0.transfer(caller, amount0);
            token1.transfer(caller, amount1);
            (amount0, amount1)
        }
    }
}

Juega con este contrato en Remix.

Last change: 2023-11-20, commit: 3890c7b

Writing to any storage slot

On Starknet, a contract's storage is a map with 2^251 slots, where each slot is a felt which is initialized to 0. The address of storage variables is computed at compile time using the formula: storage variable address := pedersen(keccak(variable name), keys). Interactions with storage variables are commonly performed using the self.var.read() and self.var.write() functions.

Nevertheless, we can use the storage_write_syscall and storage_read_syscall syscalls, to write to and read from any storage slot. This is useful when writing to storage variables that are not known at compile time, or to ensure that even if the contract is upgraded and the computation method of storage variable addresses changes, they remain accessible.

En el siguiente ejemplo, utilizamos la función hash Poseidon para calcular la dirección de una variable de almacenamiento. Poseidon es una función hash compatible con ZK que es más barata y rápida que Pedersen, lo que la convierte en una excelente elección para cálculos en cadena. Una vez calculada la dirección, utilizamos las llamadas al sistema de almacenamiento para interactuar con ella.

#[starknet::interface]
trait IWriteToAnySlots<TContractState> {
    fn write_slot(ref self: TContractState, value: u32);
    fn read_slot(self: @TContractState) -> u32;
}

#[starknet::contract]
mod WriteToAnySlot {
    use starknet::syscalls::{storage_read_syscall, storage_write_syscall};
    use starknet::SyscallResultTrait;
    use poseidon::poseidon_hash_span;
    use starknet::storage_access::Felt252TryIntoStorageAddress;
    use starknet::StorageAddress;

    #[storage]
    struct Storage {}

    const SLOT_NAME: felt252 = 'test_slot';

    #[abi(embed_v0)]
    impl WriteToAnySlot of super::IWriteToAnySlots<ContractState> {
        fn write_slot(ref self: ContractState, value: u32) {
            storage_write_syscall(0, get_address_from_name(SLOT_NAME), value.into());
        }

        fn read_slot(self: @ContractState) -> u32 {
            storage_read_syscall(0, get_address_from_name(SLOT_NAME))
                .unwrap_syscall()
                .try_into()
                .unwrap()
        }
    }
    fn get_address_from_name(variable_name: felt252) -> StorageAddress {
        let mut data: Array<felt252> = ArrayTrait::new();
        data.append(variable_name);
        let hashed_name: felt252 = poseidon_hash_span(data.span());
        let MASK_250: u256 = 0x03ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
        // By taking the 250 least significant bits of the hash output, we get a valid 250bits storage address.
        let result: felt252 = (hashed_name.into() & MASK_250).try_into().unwrap();
        let result: StorageAddress = result.try_into().unwrap();
        result
    }
}

Visita el contrato en Voyager o juega con él en Remix.

Last change: 2023-10-12, commit: 90aa7c0

Storing Arrays

En Starknet, los valores complejos (por ejemplo, tuplas o structs), se almacenan en un segmento continuo a partir de la dirección de la variable de almacenamiento. Hay una limitación de 256 elementos de campo para el tamaño máximo de un valor de almacenamiento complejo, lo que significa que para almacenar matrices de más de 255 elementos en el almacenamiento, necesitaríamos dividirlo en segmentos de tamaño n <= 255 y almacenar estos segmentos en múltiples direcciones de almacenamiento. Actualmente no hay soporte nativo para almacenar arrays en Cairo, por lo que necesitarás escribir tu propia implementación del rasgo Store para el tipo de array que desees almacenar.

Note: While storing arrays in storage is possible, it is not always recommended, as the read and write operations can get very costly. For example, reading an array of size n requires n storage reads, and writing to an array of size n requires n storage writes. If you only need to access a single element of the array at a time, it is recommended to use a LegacyMap and store the length in another variable instead.

El siguiente ejemplo muestra cómo escribir una implementación sencilla del rasgo StorageAccess para el tipo Array<felt252>, permitiéndonos almacenar arrays de hasta 255 elementos felt252.

impl StoreFelt252Array of Store<Array<felt252>> {
    fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult<Array<felt252>> {
        StoreFelt252Array::read_at_offset(address_domain, base, 0)
    }

    fn write(
        address_domain: u32, base: StorageBaseAddress, value: Array<felt252>
    ) -> SyscallResult<()> {
        StoreFelt252Array::write_at_offset(address_domain, base, 0, value)
    }

    fn read_at_offset(
        address_domain: u32, base: StorageBaseAddress, mut offset: u8
    ) -> SyscallResult<Array<felt252>> {
        let mut arr: Array<felt252> = ArrayTrait::new();

        // Read the stored array's length. If the length is superior to 255, the read will fail.
        let len: u8 = Store::<u8>::read_at_offset(address_domain, base, offset)
            .expect('Storage Span too large');
        offset += 1;

        // Sequentially read all stored elements and append them to the array.
        let exit = len + offset;
        loop {
            if offset >= exit {
                break;
            }

            let value = Store::<felt252>::read_at_offset(address_domain, base, offset).unwrap();
            arr.append(value);
            offset += Store::<felt252>::size();
        };

        // Return the array.
        Result::Ok(arr)
    }

    fn write_at_offset(
        address_domain: u32, base: StorageBaseAddress, mut offset: u8, mut value: Array<felt252>
    ) -> SyscallResult<()> {
        // // Store the length of the array in the first storage slot.
        let len: u8 = value.len().try_into().expect('Storage - Span too large');
        Store::<u8>::write_at_offset(address_domain, base, offset, len);
        offset += 1;

        // Store the array elements sequentially
        loop {
            match value.pop_front() {
                Option::Some(element) => {
                    Store::<felt252>::write_at_offset(address_domain, base, offset, element);
                    offset += Store::<felt252>::size();
                },
                Option::None(_) => { break Result::Ok(()); }
            };
        }
    }

    fn size() -> u8 {
        255 * Store::<felt252>::size()
    }
}

A continuación, puede importar esta implementación en su contrato y utilizarla para almacenar arrays en el almacenamiento:

#[starknet::interface]
trait IStoreArrayContract<TContractState> {
    fn store_array(ref self: TContractState, arr: Array<felt252>);
    fn read_array(self: @TContractState) -> Array<felt252>;
}

#[starknet::contract]
mod StoreArrayContract {
    use super::StoreFelt252Array;

    #[storage]
    struct Storage {
        arr: Array<felt252>
    }

    #[abi(embed_v0)]
    impl StoreArrayImpl of super::IStoreArrayContract<ContractState> {
        fn store_array(ref self: ContractState, arr: Array<felt252>) {
            self.arr.write(arr);
        }

        fn read_array(self: @ContractState) -> Array<felt252> {
            self.arr.read()
        }
    }
}

Visita el contrato en Voyager o juega con él en Remix.

Last change: 2023-10-12, commit: 90aa7c0

Structs as mapping keys

Para utilizar estructuras como mapping keys, puede utilizar #[derive(Hash)] en la definición de la estructura. Esto generará automáticamente una función hash para la estructura que se puede utilizar para representar la estructura como una key en un LegacyMap.

Consider the following example in which we would like to use an object of type Pet as a key in a LegacyMap. The Pet struct has three fields: name, age and owner. We consider that the combination of these three fields uniquely identifies a pet.

#[derive(Copy, Drop, Serde, Hash)]
struct Pet {
    name: felt252,
    age: u8,
    owner: felt252,
}

#[starknet::interface]
trait IPetRegistry<TContractState> {
    fn register_pet(ref self: TContractState, key: Pet, timestamp: u64);
    fn get_registration_date(self: @TContractState, key: Pet) -> u64;
}

#[starknet::contract]
mod PetRegistry {
    use hash::{HashStateTrait, Hash};
    use super::Pet;

    #[storage]
    struct Storage {
        registration_time: LegacyMap::<Pet, u64>,
    }

    #[abi(embed_v0)]
    impl PetRegistry of super::IPetRegistry<ContractState> {
        fn register_pet(ref self: ContractState, key: Pet, timestamp: u64) {
            self.registration_time.write(key, timestamp);
        }

        fn get_registration_date(self: @ContractState, key: Pet) -> u64 {
            self.registration_time.read(key)
        }
    }
}

Juega con este contrato en Remix.

Last change: 2023-10-12, commit: 90aa7c0

Hash Solidity Compatible

Este contrato demuestra el hashing Keccak en Cairo para que coincida con el keccak256 de Solidity. Mientras que ambos usan Keccak, su endianness difiere: Cairo es little-endian, Solidity big-endian. El contrato consigue la compatibilidad haciendo hashing en big-endian usando keccak_u256s_be_inputs, e invirtiendo los bytes del resultado con u128_byte_reverse.

Por ejemplo:

#[starknet::interface]
trait ISolidityHashExample<TContractState> {
    fn hash_data(ref self: TContractState, input_data: Span<u256>) -> u256;
}


#[starknet::contract]
mod SolidityHashExample {
    use keccak::{keccak_u256s_be_inputs};
    use array::Span;

    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl SolidityHashExample of super::ISolidityHashExample<ContractState> {
        fn hash_data(ref self: ContractState, input_data: Span<u256>) -> u256 {
            let hashed = keccak_u256s_be_inputs(input_data);

            // Split the hashed value into two 128-bit segments
            let low: u128 = hashed.low;
            let high: u128 = hashed.high;

            // Reverse each 128-bit segment
            let reversed_low = integer::u128_byte_reverse(low);
            let reversed_high = integer::u128_byte_reverse(high);

            // Reverse merge the reversed segments back into a u256 value
            let compatible_hash = u256 { low: reversed_high, high: reversed_low };

            compatible_hash
        }
    }
}

Juega con el contrato en Remix.

Last change: 2023-11-21, commit: 58cde22

Optimisations

Una colección de patrones de optimización para ahorrar gas y pasos.

Last change: 2023-10-12, commit: 90aa7c0

Storage optimisation

A smart contract has a limited amount of storage slots. Each slot can store a single felt252 value. Writing to a storage slot has a cost, so we want to use as few storage slots as possible.

In Cairo, every type is derived from the felt252 type, which uses 252 bits to store a value. This design is quite simple, but it does have a drawback: it is not storage efficient. For example, if we want to store a u8 value, we need to use an entire slot, even though we only need 8 bits.

Packing

Cuando almacenamos múltiples valores, podemos utilizar una técnica llamada packing. El empaquetado es una técnica que nos permite almacenar múltiples valores en un único valor de felt. Esto se hace utilizando los bits del valor de felt para almacenar múltiples valores.

Por ejemplo, si queremos almacenar dos valores u8, podemos utilizar los primeros 8 bits del valor de felt para almacenar el primer valor u8, y los últimos 8 bits para almacenar el segundo valor u8. De esta forma, podemos almacenar dos valores u8 en un único valor de felt.

Cairo proporciona un almacén incorporado que usa empaquetado que puedes usar con el trait StorePacking.

trait StorePacking<T, PackedT> {
    fn pack(value: T) -> PackedT;
    fn unpack(value: PackedT) -> T;
}

Esto permite almacenar el tipo T empaquetándolo primero en el tipo PackedT con la función pack, y luego almacenando el valor PackedT con su implementación Store. Al leer el valor, primero recuperamos el valor PackedT, y luego lo desempaquetamos en el tipo T utilizando la función unpack.

He aquí un ejemplo de almacenamiento de una estructura Time con dos valores u8 utilizando el trait StorePacking:

#[starknet::interface]
trait ITime<TContractState> {
    fn set(ref self: TContractState, value: TimeContract::Time);
    fn get(self: @TContractState) -> TimeContract::Time;
}

#[starknet::contract]
mod TimeContract {
    use starknet::storage_access::StorePacking;
    use integer::{
        U8IntoFelt252, Felt252TryIntoU16, U16DivRem, u16_as_non_zero, U16IntoFelt252,
        Felt252TryIntoU8
    };
    use traits::{Into, TryInto, DivRem};
    use option::OptionTrait;
    use serde::Serde;

    #[storage]
    struct Storage {
        time: Time
    }

    #[derive(Copy, Serde, Drop)]
    struct Time {
        hour: u8,
        minute: u8
    }

    impl TimePackable of StorePacking<Time, felt252> {
        fn pack(value: Time) -> felt252 {
            let msb: felt252 = 256 * value.hour.into();
            let lsb: felt252 = value.minute.into();
            return msb + lsb;
        }
        fn unpack(value: felt252) -> Time {
            let value: u16 = value.try_into().unwrap();
            let (q, r) = U16DivRem::div_rem(value, u16_as_non_zero(256));
            let hour: u8 = Into::<u16, felt252>::into(q).try_into().unwrap();
            let minute: u8 = Into::<u16, felt252>::into(r).try_into().unwrap();
            return Time { hour, minute };
        }
    }

    #[abi(embed_v0)]
    impl TimeContract of super::ITime<ContractState> {
        fn set(ref self: ContractState, value: Time) {
            // This will call the pack method of the TimePackable trait
            // and store the resulting felt252
            self.time.write(value);
        }
        fn get(self: @ContractState) -> Time {
            // This will read the felt252 value from storage
            // and return the result of the unpack method of the TimePackable trait
            return self.time.read();
        }
    }
}

Juega con este contrato en Remix.

Last change: 2023-10-12, commit: 90aa7c0

List

Por defecto, no hay ningún tipo de lista soportado en Cairo, pero puedes usar Alexandria. Puede consultar la documentación de Alexandria para más detalles.

What is List?

Una secuencia ordenada de valores que puede utilizarse en el storage de Starknet:

#[storage]
stuct Storage {
  amounts: List<u128>
}

Interface

trait ListTrait<T> {
  fn len(self: @List<T>) -> u32;
  fn is_empty(self: @List<T>) -> bool;
  fn append(ref self: List<T>, value: T) -> u32;
  fn get(self: @List<T>, index: u32) -> Option<T>;
  fn set(ref self: List<T>, index: u32, value: T);
  fn pop_front(ref self: List<T>) -> Option<T>;
  fn array(self: @List<T>) -> Array<T>;
}

List también implementa IndexView para que puedas utilizar la notación familiar de corchetes para acceder a sus miembros:

let second = self.amounts.read()[1];

Tenga en cuenta que, a diferencia de get, el uso de esta notación de corchetes entra en pánico cuando se accede a un índice fuera de los límites.

Support for custom types

List soporta la mayoría de los tipos de corelib. Si quieres almacenar un tipo personalizado en una List, tiene que implementar el trait Store. Puedes hacer que el compilador lo derive por ti usando el atributo #[derive(starknet::Store)].

Caveats

Hay dos particularidades que debe tener en cuenta al utilizar List

  1. The append operation costs 2 storage writes - one for the value itself and another one for updating the List's length
  2. Due to a compiler limitation, it is not possible to use mutating operations with a single inline statement. For example, self.amounts.read().append(42); will not work. You have to do it in 2 steps:
let mut amounts = self.amounts.read();
amounts.append(42);

Dependencies

Actualice las dependencias de su proyecto en el archivo Scarb.toml:

[dependencies]
(...)
alexandria_storage = { git = "https://github.com/keep-starknet-strange/alexandria.git" }

Por ejemplo, utilicemos List para crear un contrato que realice el seguimiento de una lista de importes y tareas:

#[starknet::interface]
trait IListExample<TContractState> {
    fn add_in_amount(ref self: TContractState, number: u128);
    fn add_in_task(ref self: TContractState, description: felt252, status: felt252);
    fn is_empty_list(self: @TContractState) -> bool;
    fn list_length(self: @TContractState) -> u32;
    fn get_from_index(self: @TContractState, index: u32) -> u128;
    fn set_from_index(ref self: TContractState, index: u32, number: u128);
    fn pop_front_list(ref self: TContractState);
    fn array_conversion(self: @TContractState) -> Array<u128>;
}

#[starknet::contract]
mod ListExample {
    use alexandria_storage::list::{List, ListTrait};

    #[storage]
    struct Storage {
        amount: List<u128>,
        tasks: List<Task>
    }

    #[derive(Copy, Drop, Serde, starknet::Store)]
    struct Task {
        description: felt252,
        status: felt252
    }


    #[abi(embed_v0)]
    impl ListExample of super::IListExample<ContractState> {
        fn add_in_amount(ref self: ContractState, number: u128) {
            let mut current_amount_list = self.amount.read();
            current_amount_list.append(number);
        }

        fn add_in_task(ref self: ContractState, description: felt252, status: felt252) {
            let new_task = Task { description: description, status: status };
            let mut current_tasks_list = self.tasks.read();
            current_tasks_list.append(new_task);
        }

        fn is_empty_list(self: @ContractState) -> bool {
            let mut current_amount_list = self.amount.read();
            current_amount_list.is_empty()
        }

        fn list_length(self: @ContractState) -> u32 {
            let mut current_amount_list = self.amount.read();
            current_amount_list.len()
        }

        fn get_from_index(self: @ContractState, index: u32) -> u128 {
            self.amount.read()[index]
        }

        fn set_from_index(ref self: ContractState, index: u32, number: u128) {
            let mut current_amount_list = self.amount.read();
            current_amount_list.set(index, number);
        }

        fn pop_front_list(ref self: ContractState) {
            let mut current_amount_list = self.amount.read();
            current_amount_list.pop_front();
        }

        fn array_conversion(self: @ContractState) -> Array<u128> {
            let mut current_amount_list = self.amount.read();
            current_amount_list.array()
        }
    }
}
Last change: 2023-11-27, commit: e112ed3