Este artículo muestra cómo construir un contrato de Cairo desplegable para Starknet. Comenzando con un boceto simple, añadiremos características gradualmente para construir un contrato funcional que demuestre los bloques de construcción fundamentales de un contrato de Cairo.
El contrato tendrá una variable de contador que se puede incrementar en cualquier cantidad y una función para obtener su valor.
La primera versión de nuestro contrato
mod Counter {
fn increase_counter(amount: felt252) {
// TODO
}
fn get_counter() -> felt252 {
// TODO
}
}
El código anterior incluye las siguientes características:
- Un bloque de módulo, denotado por la palabra clave
mod. Cada contrato de Cairo se escribe dentro de un módulo. Esto es similar a la palabra clavecontracten Solidity, y el nombre del módulo puede ser cualquiera. - Dos funciones: una para incrementar el contador y otra para recuperar su valor actual.
Añadiendo una “interfaz” mediante la definición de un trait para el contrato Counter
En Solidity, una interfaz define un conjunto de funciones que un contrato debe implementar. Las interfaces no son obligatorias para los contratos, pero se recomienda su uso.
En Cairo, esta misma idea se representa utilizando un trait, el cual define una lista de funciones sin proporcionar su implementación. En ese sentido, un trait en Cairo desempeña el mismo papel que una interfaz en Solidity.
Sin embargo, es importante aclarar que un trait por sí solo no se trata automáticamente como la interfaz de un contrato. Necesitamos marcar explícitamente el trait como una interfaz para que sea tratado como tal, y eso se hace mediante una anotación que veremos en una sección posterior.
Por ahora, piénsalo de esta manera:
- el trait describe qué funciones debe tener un contrato,
- y la anotación (que presentaremos en breve) le dice al trait cómo debe comportarse, en este caso, como la interfaz de un contrato.
No es posible implementar las funciones que no forman parte de la interfaz definida - más adelante veremos otra opción para implementar funciones adicionales.
El siguiente código amplía el contrato Counter al definir un trait y proporcionar las implementaciones para las funciones declaradas dentro de él:
// Define a trait with two functions
pub trait ICounter {
fn increase_counter(amount: felt252);
fn get_counter() -> felt252;
}
mod Counter {
// Implement the functions within the `ICounter` trait
impl CounterImpl of super::ICounter {
fn increase_counter(amount: felt252) {
// TODO
}
fn get_counter() -> felt252 {
// TODO
}
}
}
Este borrador añade las siguientes características:
- Un trait público, denotado por las palabras clave
pubytrait. - Un bloque de implementación (
impl) contiene las implementaciones de las funciones. Este bloque implementa el traitICounter. El nombre del trait o del bloque de implementación puede ser cualquiera, aunque es una práctica común usar nombres descriptivos que reflejen el propósito del contrato. Por convención, Scarb sigue el patrónIContractNamepara las interfaces yContractNameImplpara la implementación correspondiente que define las funciones públicas.
Añadiendo el almacenamiento
A continuación, necesitamos un lugar para almacenar el valor del contador. Dado que el valor del contador necesita persistir entre transacciones, debe almacenarse como parte del estado del contrato. En Cairo, los datos persistentes se almacenan dentro de un único struct llamado Storage que agrupa todas las variables de estado juntas.
// Storage traits
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
struct Storage {
counter: felt252,
}
Esto incluye las siguientes características:
- Una declaración
useque importa los traits requeridos para leer y escribir en el almacenamiento del contrato. - Una nueva estructura para el almacenamiento del contrato, definida con la palabra clave
struct. El struct debe llamarseStorage. - La propia variable
counterdentro del almacenamiento.
Añadiendo estado y lógica
Ahora que tenemos almacenamiento, nuestras funciones necesitan una forma de interactuar con él. En Cairo, el estado del contrato no está disponible automáticamente; debe pasarse explícitamente a las funciones como un parámetro.
Para facilitar esto, Cairo utiliza una referencia de estado, un parámetro específico que representa y permite el acceso al almacenamiento del contrato.
Hay dos formas de definir una referencia de estado en Cairo: una que proporciona acceso de lectura y escritura al almacenamiento, y otra que proporciona acceso de solo lectura. Así es cómo usarlas:
- Acceso de lectura y escritura: usa una variable de referencia con la palabra clave
ref. - Acceso de solo lectura: usa una variable de instantánea (snapshot) con el símbolo
@. Esto es similar a las funcionesviewde Solidity, donde la función puede leer pero no modificar el almacenamiento del contrato.
Nota que la función increase_counter usa la palabra clave ref en su parámetro para obtener acceso de lectura y escritura al estado del contrato, mientras que la función get_counter usa el símbolo @ para obtener acceso de solo lectura, como se muestra en el código a continuación:
pub trait ICounter<TContractState> {
// Function that can read and modify the contract's state
fn increase_counter(ref self: TContractState, amount: felt252);
// Function that can only read from the contract's state
fn get_counter(self: @TContractState) -> felt252;
}
mod Counter {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
struct Storage {
counter: felt252,
}
impl CounterImpl of super::ICounter<ContractState> {
// Uses `ref self`: gives read and write access to the storage
fn increase_counter(ref self: ContractState, amount: felt252) {
self.counter.write(self.counter.read() + amount);
}
// Uses `@`: gives read-only access to the storage
fn get_counter(self: @ContractState) -> felt252 {
self.counter.read()
}
}
}
Los cambios que hemos hecho hasta ahora al contrato nos permiten interactuar directamente con el almacenamiento a través del estado del contrato. Los cambios principales son los siguientes:
- Se añadió el parámetro de tipo
TContractStateal trait como un marcador de posición, NO como un tipo real, para que pueda funcionar con el diseño de estado de cualquier contrato, en lugar de estar atado a uno específico.pub trait ICounter {se convirtió enpub trait ICounter<TContractState> {
- En el bloque impl, el marcador de posición
TContractStatees reemplazado por el tipo real del estado del contrato (ContractState):impl CounterImpl of super::ICounter {se convirtió enimpl CounterImpl of super::ICounter<ContractState> {
- Se añadió una referencia al estado en ambas funciones. Una tiene acceso de escritura y la otra solo tiene acceso de lectura al almacenamiento:
fn increase_counter(amount: felt252) {se convirtió enfn increase_counter(ref self: ContractState, amount: felt252) {fn get_counter() -> felt252 {se convirtió enfn get_counter(self: @ContractState) -> felt252 {
- Se añadió la lógica para incrementar el contador y leerlo utilizando
self, que es del tipoContractState, representando el estado del contrato:- Se añadió la lógica
self.counter.write(self.counter.read() + amount); - Se añadió la lógica
self.counter.read()
- Se añadió la lógica
Finalizando el contrato con anotaciones
Cairo utiliza diferentes anotaciones (también llamadas atributos) para indicar cómo deben comportarse las distintas partes del contrato. Estas anotaciones especifican cosas como:
- qué trait define la interfaz,
- qué módulo es un contrato desplegable,
- qué struct es el struct de almacenamiento,
- y qué bloque de implementación expone funciones al mundo exterior.
Cada anotación en Cairo comienza con #[] y se coloca directamente encima del código al que se aplica. Por ejemplo, colocar el atributo #[starknet::interface] en una parte del código indica que debe tratarse como la interfaz del contrato.
Aquí está el contrato completo con sus anotaciones:
#[starknet::interface]
pub trait ICounter<TContractState> {
fn increase_counter(ref self: TContractState, amount: felt252);
fn get_counter(self: @TContractState) -> felt252;
}
#[starknet::contract]
mod Counter {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
counter: felt252,
}
#[abi(embed_v0)]
impl CounterImpl of super::ICounter<ContractState> {
fn increase_counter(ref self: ContractState, amount: felt252) {
self.counter.write(self.counter.read() + amount);
}
fn get_counter(self: @ContractState) -> felt252 {
self.counter.read()
}
}
}
Las anotaciones añadidas son:
#[starknet::interface]marca un trait como una interfaz. No puedes tener un bloqueimplsin una interfaz anotada.#[starknet::contract]marca un módulo como un contrato inteligente de Starknet.#[storage]indica la estructura que define el diseño de almacenamiento del contrato. Un contrato debe tener exactamente unstructde almacenamiento con esta anotación.#[abi(embed_v0)]hace que las funciones dentro del bloqueimplformen parte del ABI público del contrato — al igual que las funcionespublicoexternalen Solidity. Omitir esta anotación hace que las funciones estén disponibles solo dentro de este contrato.- El
embed_v0significa incrustar el ABI usando la versión 0 del formato ABI. Env0, los selectores de funciones se derivan únicamente del nombre de la función. El nombre delimplno se tiene en cuenta, lo que significa que si un contrato tiene diferentes bloquesimpl(implementando distintos traits) que definen una función con el mismo nombre, ocurrirá una colisión de nombres. Las versiones futuras (comov1) podrían mejorar esto al incluir más contexto en la derivación de selectores sin romper la compatibilidad hacia atrás. - En Cairo, no existen palabras clave de visibilidad, como
privateointernalde Solidity, para denotar una función privada. Otra forma de crear funciones privadas es simplemente agregarlas fuera del bloqueimpl, sin anotaciones. Se muestra un ejemplo más adelante en este artículo.
- El
Con esto en su lugar, el contrato está listo para ser compilado, desplegado y llamado desde otros contratos o clientes.
Funciones fuera de la interfaz
En Cairo, también es posible definir funciones públicas fuera de la implementación de una interfaz marcándolas con la anotación #[external(v0)]. Al igual que en #[abi(embed_v0)], el v0 en #[external(v0)] significa que se usa la versión 0 del ABI.
Es posible usar tanto interfaces como funciones externas anotadas en un contrato. Sin embargo, se recomienda usar interfaces porque permite que los contratos externos dependan de una definición compartida al interactuar con tu contrato.
En el siguiente código, añadiremos una nueva función increase_counter_by_five que está anotada con #[external(v0)]. Esta función puede llamarse externamente y está incluida en el ABI del contrato, a pesar de que no está definida a través de una interfaz (se comporta como las funciones públicas, pero sin una interfaz).
Esta nueva función llama a otra función nueva, privada, llamada get_five. Esta función puede llamarse únicamente dentro de este contrato.
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn increase_counter(ref self: TContractState, amount: felt252);
fn get_counter(self: @TContractState) -> felt252;
}
#[starknet::contract]
mod HelloStarknet {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
counter: felt252,
}
#[abi(embed_v0)]
impl CounterImpl of super::IHelloStarknet<ContractState> {
fn increase_counter(ref self: ContractState, amount: felt252) {
self.counter.write(self.counter.read() + amount);
}
fn get_counter(self: @ContractState) -> felt252 {
self.counter.read()
}
}
// ********* NEWLY ADDED - START ********* //
#[external(v0)]
fn increase_counter_by_five(ref self: ContractState) {
self.counter.write(self.counter.read() + get_five());
}
fn get_five() -> felt252 {
5
}
// ********* NEWLY ADDED - END ********* //
}
Nota que el primer parámetro de cualquier función
publicoexternaldebe ser una referencia al estado del contrato.
Compilando el contrato
Para asegurarnos de que nuestro código es válido y está listo para ejecutarse, debemos compilarlo.
Una herramienta popular para trabajar con código de Cairo es Scarb — un gestor de paquetes y sistema de construcción de Cairo. Si aún no lo has hecho, instálalo siguiendo las instrucciones en el artículo Cairo for Solidity developers.
Una vez instalado, puedes crear y compilar tu proyecto de contrato con los siguientes pasos:
- Inicializa un nuevo proyecto ejecutando
scarb new counter, y continúa con el ejecutor de pruebas predeterminado cuando se te solicite. - Navega a la carpeta del proyecto:
cd counter. - Reemplaza el contenido de
src/lib.cairocon nuestro contrato. - Compila el contrato:
scarb build.
Si obtienes errores de compilación similares a Type annotations needed, asegúrate de que tu Scarb.toml tenga starknet = "2.12.0" añadido bajo la sección [dependencies].
Probando el contrato
Scarb también genera un contrato de prueba después de inicializar un nuevo proyecto. Las pruebas están escritas directamente en Cairo y se ejecutan localmente para probar la lógica real del contrato antes de desplegarlo on-chain.
Para ver la prueba, navega a ./tests/test_contract.cairo. A continuación, se muestra un desglose de lo que sucede en la prueba generada.
Importaciones

-
use starknet::ContractAddress;Esto importa
ContractAddressdesde el módulostarknet.- Importa el tipo
ContractAddress. - Esta es la representación que tiene Starknet de una dirección de contrato y es obligatoria siempre que se interactúe o se haga referencia a contratos desplegados.
- Importa el tipo
-
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait};Esto importa las herramientas necesarias para declarar y desplegar contratos durante las pruebas desde la biblioteca estándar de starknet foundry
snforge_std.declare: Se usa para declarar un contrato en el entorno de pruebas antes del despliegue. Es como enviar el código del contrato a la red.ContractClassTrait: Proporciona métodos auxiliares para interactuar con clases de contratos declaradas (como desplegarlas).DeclareResultTrait: Expone una función en el resultado de la declaración que recupera la clase del contrato (equivalente al bytecode del contrato en Solidity).
-
use counter::IHelloStarknetSafeDispatcher;yuse counter::IHelloStarknetSafeDispatcherTrait;Esto importa la versión segura de la interfaz del contrato usando el nombre del proyecto, en nuestro caso,
counter.-
IHelloStarknetSafeDispatcher: El safe dispatcher (despachador seguro) es responsable de llamar a las funciones del contrato. Pero a diferencia de Solidity, donde una llamada de función devuelve el valor directamente, aquí cada llamada devuelve un envoltorio (wrapper) que contiene el valor devuelto (si tuvo éxito) o un error (si falló).Es importante destacar que, incluso si una llamada al contrato falla, la ejecución continúa dentro de la función de prueba. Esto permite que el safe dispatcher maneje el error de manera controlada en lugar de revertir toda la transacción.
-
IHelloStarknetSafeDispatcherTrait: Expone las funciones del contrato que pueden ser llamadas para el dispatcher. El valor de retorno de cada función está envuelto, indicando que podría tener éxito o fallar.
-
-
use counter::IHelloStarknetDispatcher;yuse counter::IHelloStarknetDispatcherTrait;Esto importa la interfaz del contrato (no la versión segura) usando el nombre del proyecto, en nuestro caso,
counter.IHelloStarknetDispatcher: El dispatcher también llama a las funciones del contrato. Sin embargo, a diferencia de la versión segura, devuelve directamente el valor de la función sin ningún envoltorio. Si el contrato de destino falla, la llamada entra en pánico (panic) inmediatamente, causando que la ejecución se detenga en la función de prueba y previniendo cualquier forma de manejo de errores controlado.IHelloStarknetDispatcherTrait: Expone las funciones del contrato que pueden ser llamadas para el dispatcher. Cada función devuelve los tipos de retorno puros (raw) de la interfaz.
Función de despliegue

Esta función toma el nombre del contrato (en nuestro caso, HelloStarknet) como argumento, despliega el contrato y devuelve su dirección de contrato.
Nota: el nombre del contrato es el identificador que viene después de la palabra clave mod dentro del archivo lib.cairo (mod HelloStarknet), mientras que el nombre del proyecto (como counter) es simplemente el nombre de la carpeta creada al inicializar el proyecto con Scarb.
A continuación se detalla lo que sucede en la función:
declare(name)- Esto toma el nombre del contrato (generalmente proporcionado como un array de bytes) y lo declara en la red Starknet.
.contract_class()- Extrae la clase del contrato a partir del contrato declarado.
.deploy(@ArrayTrait::new())- Despliega la clase del contrato.
ArrayTrait::new()se usa para pasar los argumentos del constructor (aquí es un array vacío porque el constructor no toma parámetros).- Devuelve una tupla donde el primer elemento es la dirección del contrato.
- Valor de retorno
- La función devuelve la dirección del contrato recién desplegado.
Casos de prueba

En la captura de pantalla anterior, hay dos casos de prueba:
test_increase_balance: Usa el dispatcher normal para llamar funciones en el contrato.test_cannot_increase_balance_with_zero_value: Usa el safe dispatcher para llamar funciones en el contrato.
Comando de prueba
Ejecuta el siguiente comando para probar:
scarb test
Resumen de similitudes y diferencias clave
En este artículo, hemos enumerado múltiples similitudes entre Cairo y Solidity, pero también varias diferencias. Para mayor claridad, las comparaciones son las siguientes:
- La palabra clave
modde Cairo desempeña un papel similar a la palabra clavecontractde Solidity. - Las interfaces de Cairo se definen usando un trait anotado con
#[starknet::interface], al igual queinterfaceen Solidity. - Para crear funciones de solo lectura en Cairo, como
viewen Solidity, pasa el estado como una instantánea usando el símbolo@. - Para crear funciones tipo
purede Solidity en Cairo, define la función como lo hicimos con la funciónget_five. - Para hacer que una función pueda llamarse externamente, como con
publicyexternalen Solidity, usa#[external(v0)]o impleméntala en un bloqueimplcon#[abi(embed_v0)].
Conclusión
Los contratos de Solidity y Cairo sirven para propósitos muy similares. Si bien la sintaxis de Cairo es diferente, muchos de los conceptos centrales resultarán familiares para los desarrolladores de Solidity.
La estructura analizada en este artículo es un enfoque posible, pero no es la única opción arquitectónica proporcionada por Starknet. En los próximos artículos de esta serie, exploraremos diseños alternativos para ayudarte a comprender mejor la flexibilidad que Cairo y Starknet ofrecen para la construcción de contratos inteligentes escalables y componibles.
Próximos pasos
Para continuar aprendiendo sobre los contratos de Cairo, te animamos a probar y jugar con los ejercicios en nuestro repositorio de GitHub.
Este artículo es parte de una serie de tutoriales sobre Cairo Programming on Starknet