Un constructor es una función de una sola llamada que se ejecuta durante el despliegue del contrato para inicializar variables de estado, realizar tareas de configuración del contrato, realizar interacciones entre contratos, entre otros.
En Cairo, los constructores se definen utilizando el atributo #[constructor] dentro del bloque mod de un contrato.
Este artículo cubrirá cómo funcionan los constructores en Cairo, la serialización manual y automática de los argumentos del constructor dentro de las pruebas de Scarb, y cómo los valores de retorno del constructor difieren de los de Solidity.
Un constructor simple en Cairo
Tomemos un contrato simple de Solidity que inicializa una variable de estado count en su constructor:
contract Counter {
uint256 public count;
constructor(uint256 _count) {
count = _count;
}
}
Aquí está la versión equivalente en Cairo:
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn get_count(self: @TContractState) -> felt252;
}
#[starknet::contract]
mod HelloStarknet {
use starknet::storage::{StoragePointerReadAccess,StoragePointerWriteAccess};
#[storage]
struct Storage {
count: felt252
}
// ************ CONSTRUCTOR FUNCTION ************* //
#[constructor]
fn constructor(ref self: ContractState, _count: felt252) {
self.count.write(_count);
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn get_count(self: @ContractState) -> felt252 {
self.count.read()
}
}
}
El atributo #[constructor] en el código anterior marca la función como el constructor del contrato. La función debe llamarse constructor y se ejecuta una vez durante el despliegue. Toma ref self para permitir el acceso de escritura al almacenamiento del contrato, junto con cualquier parámetro necesario para la inicialización, en este caso, _count.
El constructor anterior simplemente inicializa la variable de almacenamiento count con el valor pasado como argumento.
Probémoslo, crea un nuevo proyecto con Scarb:
scarb new counter
A continuación, reemplaza el código generado en el archivo src/lib.cairo con el código del contrato HelloStarknet de arriba.
Para verificar que count se inicializa correctamente, escribiremos una prueba que despliega el contrato con un valor específico y luego comprueba que el valor almacenado coincide con el que pasamos.
Abre el archivo de prueba (tests/test_contract.cairo), luego reemplaza el código generado con el siguiente:
use counter::{IHelloStarknetDispatcher, IHelloStarknetDispatcherTrait};
use snforge_std::{ContractClassTrait, DeclareResultTrait, declare};
use starknet::ContractAddress;
fn deploy_contract(name: ByteArray) -> ContractAddress {
let contract = declare(name).unwrap().contract_class();
// CREATE ARGUMENT ARRAY FOR CONSTRUCTOR. WE'RE PASSING `5` AS THE INITIAL VALUE FOR `count`
let mut args = ArrayTrait::new();
args.append(5_felt252);
// DEPLOY THE CONTRACT WITH THE PROVIDED CONSTRUCTOR ARGUMENTS
let (contract_address, _) = contract.deploy(@args).unwrap();
contract_address // Return address of deployed contract
}
#[test]
fn test_count() {
let contract_address = deploy_contract("HelloStarknet");
let dispatcher = IHelloStarknetDispatcher { contract_address };
// CALL THE `get_count` FUNCTION TO READ THE CURRENT VALUE OF `count`
let result = dispatcher.get_count();
// ASSERT THAT THE INITIALIZED VALUE MATCHES WHAT WE PASSED DURING DEPLOYMENT
assert!(result == 5, "failed {} != 5", result);
}
La parte clave de esta prueba es cómo pasamos el argumento del constructor como un array de valores felt252 durante el despliegue. Esta parte es fácil pasarla por alto, pero es importante; agregamos el valor 5 al array args antes de llamar a deploy, que es como inicializamos el contrato con 5.
// CREATE ARGUMENT ARRAY FOR CONSTRUCTOR. WE'RE PASSING `5` AS THE INITIAL VALUE FOR `count`
let mut args = ArrayTrait::new();
args.append(5_felt252);
// DEPLOY THE CONTRACT WITH THE PROVIDED CONSTRUCTOR ARGUMENTS
let (contract_address, _) = contract.deploy(@args).unwrap();
El resto de la prueba confirma que esta inicialización funcionó como se esperaba. Llamamos a get_count, que devuelve el valor actual de la variable count, y luego verificamos que sea igual a 5.
A diferencia de las pruebas de Foundry en Solidity, donde los argumentos del constructor pueden ser de varios tipos (enteros, cadenas, arrays, structs, direcciones, etc.), el despliegue de contratos dentro de las pruebas de Scarb requiere que todos los argumentos del constructor se pasen como valores felt252. Esto se debe a que la Starknet VM es un sistema basado en felt252, todo se codifica y se transmite como felts internamente.
Dicho esto, no siempre necesitamos convertir manualmente cada argumento en felts nosotros mismos durante el despliegue en las pruebas. Starknet Foundry proporciona una función auxiliar que maneja automáticamente la serialización de los argumentos del constructor y el despliegue del contrato en las pruebas. Cubriremos eso en breve, pero antes de usar la función auxiliar, aprenderemos cómo serializar tipos primitivos y complejos para saber qué está haciendo la función auxiliar en segundo plano.
Pasando tipos primitivos distintos a felt252 al constructor
Como se mencionó anteriormente, podemos serializar manualmente valores que no son felt252 (como ContractAddress) a felt252 durante el despliegue, pasarlos como un array de felt252 y luego decodificarlos en el constructor para inicializar el estado del contrato. Veamos un ejemplo para ver cómo funciona esto en la práctica.
La versión en Solidity:
contract SomeContract {
uint256 count;
address owner;
bool isActive;
constructor(uint256 _count, address _owner, bool _isActive) {
count = _count;
owner = _owner;
isActive = _isActive;
}
}
Aquí está el equivalente en Cairo:
#[starknet::contract]
mod HelloStarknet {
use starknet::ContractAddress;
use starknet::storage::{StoragePointerWriteAccess};
// Define the contract's storage.
#[storage]
struct Storage {
count: u256,
owner: ContractAddress,
is_active: bool,
}
// CONSTRUCTOR FUNCTION
#[constructor]
fn constructor(
ref self: ContractState,
_count: u256,
_owner: ContractAddress,
_is_active: bool
) {
// INIT STATE VARS
self.count.write(_count);
self.owner.write(_owner);
self.is_active.write(_is_active);
}
}
Aquí el constructor acepta tres argumentos:
_count: El valor de recuento de 256 bits._owner: La dirección del propietario del contrato._is_active: Una bandera booleana que indica si el contrato debe comenzar en un estado activo.
Luego escribe cada uno de los argumentos proporcionados en sus correspondientes variables de almacenamiento utilizando el método .write(), inicializando así los estados en el despliegue.
Serializando manualmente los argumentos del constructor antes del despliegue
En la prueba, la función de abajo deploy_contract muestra cómo los valores que no son de tipo felt252 se serializan “manualmente” antes de ser pasados como argumentos del constructor:
fn deploy_contract(name: ByteArray) -> ContractAddress {
let contract = declare(name).unwrap().contract_class();
// CREATE ARGUMENT ARRAY FOR CONSTRUCTOR
let mut args = ArrayTrait::new();
// VALUES TO SERIALIZE
let count: u256 = 115792089237316195423570985008687907853269984665640564039457584007913129639935; // count = max value of u256
let owner: ContractAddress = 0xbeef.try_into().unwrap();
let is_active: bool = true;
// Serialize the u256 `count` value into two felt252 elements (low, high)
// and push them into the constructor `args` array.
count.serialize(ref args);
// SERIALIZE INTO FELT252, THEN PUSH TO `args` ARRAY
owner.serialize(ref args); // ContractAddress -> felt252
is_active.serialize(ref args); // bool -> felt252
// DEPLOY THE CONTRACT WITH THE PROVIDED CONSTRUCTOR ARGUMENTS
let (contract_address, _) = contract.deploy(@args).unwrap();
contract_address
}
Al tratar con múltiples argumentos del constructor en Starknet, estos deben pasarse como un Array<felt252>.
En este ejemplo, dado que la variable count es de tipo u256, el método .serialize() divide su valor en dos mitades de 128 bits; low y high antes de ser añadidos al array args. Como dijimos antes, esto se debe a que un solo felt252 no puede contener de forma segura un entero completo de 256 bits. Al serializar los valores correctamente (codificando cada mitad como un felt252 separado), la función del constructor puede deserializarlos automáticamente en el valor u256 original.
Los valores restantes del constructor; owner (un ContractAddress) y is_active (un bool) caben cada uno en un solo felt252, por lo que serializarlos es sencillo. Serializarlos en el orden exacto que coincide con la secuencia de parámetros del constructor es importante ya que el despliegue fallará o producirá un comportamiento inesperado si los argumentos no se alinean o corresponden con el orden de parámetros esperado del constructor.
El paso final utiliza el operador @ al pasar el array a contract.deploy(@args), que es la forma en que Starknet pasa datos de un array sin transferir la propiedad.
En la siguiente sección, veremos cómo automatizar la serialización de los argumentos del constructor en las pruebas usando una función auxiliar que realiza todos los pasos de serialización anteriores por nosotros.
Pasando tipos complejos
Al igual que los tipos primitivos, los argumentos complejos del constructor también deben serializarse en un array de valores felt252 antes del despliegue.
Consideremos el siguiente contrato de Solidity que inicializa un tipo complejo (un struct) a través del constructor:
// SPDX-License-Identifier: MIT
pragma solidity =0.8.30;
contract Bank {
struct Account {
address wallet;
uint64 balance;
}
Account userAccount;
constructor(Account memory _userAccount) {
userAccount = _userAccount;
}
}
Esto es muy sencillo en Solidity.
A continuación se muestra el equivalente en Cairo:
#[starknet::contract]
pub mod HelloStarknet {
use starknet::ContractAddress;
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
// `Account` STRUCT
#[derive(Drop, starknet::Store)]
pub struct Account {
pub wallet: ContractAddress,
pub balance: u64,
}
#[storage]
struct Storage {
// Use the `Account` struct in storage
pub user_account: Account,
}
// Constructor function
#[constructor]
// Each field is declared as its own constructor argument
fn constructor(
ref self: ContractState,
new_user_account: Account,
) {
// WRITE `new_user_account` STRUCT TO STORAGE
self.user_account.write(new_user_account);
}
}
Aquí, el módulo del contrato está marcado como pub mod, haciéndolo un módulo público. Esto permite que código externo, como pruebas u otros módulos, accedan a los elementos definidos dentro de él, como el struct Account. Dado que Account también se declara como pub, se vuelve completamente accesible fuera del módulo del contrato, lo cual es necesario para serializar este struct en la prueba.
El trait starknet::Store derivado en el struct Account es lo que permite a Cairo serializar y deserializar correctamente el struct para el almacenamiento. Sin este trait, el compilador no sabría cómo manejar el struct al escribir o leer del almacenamiento.
Por último, el constructor toma un único parámetro, new_user_account de tipo Account, y lo escribe directamente en el almacenamiento.
Serialización automática de argumentos del constructor y despliegue con deploy_for_test
Este método maneja todo el trabajo pesado por nosotros utilizando una función auxiliar proporcionada por Starknet Foundry deploy_for_test, que serializa automáticamente las entradas del constructor antes del despliegue.
Con este enfoque, no hay necesidad de construir manualmente un Array<felt252> o llamar a .serialize() en cada argumento. En su lugar, la función auxiliar lee la firma del constructor del contrato y realiza la codificación correcta en segundo plano, asegurando que cada parámetro se serialice exactamente como el contrato espera.
La firma de la función deploy_for_test:

Parámetros de la función deploy_for_test
En el recuadro rojo, están los parámetros de deploy_for_test. Los dos primeros son:
class_hash: el hash de clase compilado del contratodeployment_params: una estructura que contiene los campos necesarios para desplegar una nueva instancia del contrato
Después de estos dos parámetros fijos, la función toma tantos argumentos del constructor como defina el contrato:
<constructor_param1>: <constructor_param_type1><constructor_param2>: <constructor_param_type2>- …
<constructor_paramN>: <constructor_param_typeN>
En otras palabras, los parámetros del 1 al N se mapean directamente a los parámetros del constructor del contrato. Por ejemplo, dado un constructor como:
#[constructor]
fn constructor(
ref self: ContractState,
count: u256,
owner: ContractAddress,
is_active: bool
) {
...
}
La llamada a deploy_for_test se vería así:
deploy_for_test(
// **** First two params - START **** //
class_hash,
deployment_params,
// **** First two params - END **** //
// **** Constructor params - START **** //
count,
owner,
is_active,
// **** Constructor params - END **** //
);
Tipo de retorno de la función deploy_for_test

En el recuadro azul está el tipo de retorno. La función devuelve un Result, que puede ser uno de dos resultados:
-
Ok(..): lo que significa que el despliegue fue exitoso.Devuelve una tupla que contiene:
- la
ContractAddressdel contrato recién desplegado, y - un
Span<felt252>que representa cualquier valor devuelto por el constructor (se cubre más adelante en este capítulo).
- la
-
Err(..): lo que significa que el despliegue falló.En este caso, la función devuelve un
Array<felt252>que contiene los datos de error emitidos por el despliegue fallido.
Ejemplo práctico
Para usar esta función en la práctica, despleguemos el contrato mostrado anteriormente, cuyo constructor toma un solo parámetro Account. Debido a que tanto el módulo (contrato) como el struct Account fueron marcados como pub, el entorno de prueba puede importarlos y serializarlos automáticamente.
A continuación, se muestra un ejemplo completo que demuestra cómo declarar el contrato, preparar los parámetros de despliegue, construir el argumento Account y finalmente desplegar el contrato usando deploy_for_test:
// ********* NEW IMPORTS - START ********* //
use myconstructor::HelloStarknet::{Account, deploy_for_test};
use starknet::deployment::DeploymentParams;
// ********** NEW IMPORTS - END ********** //
use myconstructor::{IHelloStarknetDispatcher, IHelloStarknetDispatcherTrait};
use snforge_std::{DeclareResult, DeclareResultTrait, declare};
use starknet::ContractAddress;
fn deploy_contract(name: ByteArray) -> ContractAddress {
// 1. Declare contract to get the class_hash
let declare_result: DeclareResult = declare(name).unwrap();
let class_hash = declare_result.contract_class().class_hash;
// 2. Create deployment parameters
let deployment_params = DeploymentParams { salt: 0, deploy_from_zero: true };
// 3. Create new account
let new_account = Account { wallet: 'BOB'.try_into().unwrap(), balance: 5 };
// 4. Use `deploy_for_test` to deploy the contract
// It automatically handles serialization of constructor parameters
let (_contract_address, _) = deploy_for_test(*class_hash, deployment_params, new_account)
.expect('Deployment failed');
_contract_address
}
-
use myconstructor::HelloStarknet::{Account, deploy_for_test};Aquí,
myconstructores el nombre del proyecto, yHelloStarknetes el módulo del contrato definido dentro de ese proyecto. Al importar el módulo del contrato HelloStarknet, tenemos acceso al structAccounty a la función auxiliardeploy_for_test. -
use starknet::deployment::DeploymentParams;Esta importación trae
DeploymentParams, un struct proporcionado por la biblioteca principal de Starknet. Permite configurar cómo debe desplegarse el contrato (p. ej. usando un salt personalizado o desplegando desde cero). Siempre es necesario al llamar adeploy_for_test, porque esa función espera los parámetros de despliegue como su segundo argumento.
Por último, la función deploy_contract une todo. Primero declara el contrato para obtener su class_hash, luego prepara los DeploymentParams requeridos, construye un nuevo struct Account que se pasará al constructor del contrato y finalmente llama a deploy_for_test.
Ejercicio: Resuelve el ejercicio constructor en el repositorio de the Cairo-Exercises.
Valores de retorno en los constructores
En Solidity, un constructor nunca devuelve un valor. Durante el despliegue, la EVM ejecuta el constructor y trata su única “salida” como el bytecode de tiempo de ejecución a almacenar en la cadena (on-chain).
Cairo, por otro lado, funciona de manera diferente. Después del despliegue, devuelve una tupla: (ContractAddress, Span<felt252>).
ContractAddress: la dirección del contrato desplegado.Span<felt252>: un span de valoresfelt252que contiene los datos de retorno del constructor. Cualquier tipo distinto afelt252se convierte automáticamente antes de colocarse aquí.
Para demostrar esto, vamos a inicializar un nuevo proyecto en Scarb:
scarb new rett
Luego añadimos un constructor a nuestro contrato generado en lib.cairo, de esta manera:
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn increase_balance(ref self: TContractState, amount: felt252);
fn get_balance(self: @TContractState) -> felt252;
}
#[starknet::contract]
mod HelloStarknet {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
balance: felt252,
}
// ************************ NEWLY ADDED - START ***********************//
#[constructor]
fn constructor(ref self: ContractState) -> felt252 {
33
}
// ************************ NEWLY ADDED - END ************************//
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn increase_balance(ref self: ContractState, amount: felt252) {
assert(amount != 0, 'Amount cannot be 0');
self.balance.write(self.balance.read() + amount);
}
fn get_balance(self: @ContractState) -> felt252 {
self.balance.read()
}
}
}
Para mantener las cosas simples, nuestro constructor simplemente devolverá el valor 33.
Para mostrar que el constructor realmente devuelve un valor, naveguemos al archivo de prueba test_contract.cairo y reemplacemos el código generado con este (se simplificó el código para mayor legibilidad):
use rett::{IHelloStarknetDispatcher, IHelloStarknetDispatcherTrait};
use snforge_std::{ContractClassTrait, DeclareResultTrait, declare};
use starknet::ContractAddress;
fn deploy_contract(name: ByteArray) -> ContractAddress {
let contract = declare(name).unwrap().contract_class();
let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap();
contract_address
}
#[test]
fn test_increase_balance() {
let contract_address = deploy_contract("HelloStarknet");
}
Haremos cambios en la parte resaltada en esta captura de pantalla:

El código de prueba actualizado
Lo que cambió es:
- Tipo de retorno: la función
deploy_contractahora devuelve una tupla(ContractAddress, felt252)en lugar de soloContractAddress. - Captura de la salida del constructor: Se introdujo
ret_valsde tipoSpan<felt252>para contener los valores de retorno del constructor. - Retorno de tupla: Devolvemos la dirección del contrato junto con el primer elemento de
ret_vals, ya que el constructor solo devuelve un único valor.
Finalmente, la prueba verifica que el valor de retorno del constructor es 33, confirmando que el valor se devuelve correctamente durante el despliegue.
use rett::{IHelloStarknetDispatcher, IHelloStarknetDispatcherTrait};
use snforge_std::{ContractClassTrait, DeclareResultTrait, declare};
use starknet::ContractAddress;
// Change return type to a tuple so we can capture the constructor’s return value.
fn deploy_contract(name: ByteArray) -> (ContractAddress, felt252) {
let contract = declare(name).unwrap().contract_class();
// Capture both the contract address and the constructor’s return values (as a Span<felt252>).
let (contract_address, ret_vals) = contract.deploy(@ArrayTrait::new()).unwrap();
// Return the address plus the first element in ret_vals (we expect only one value).
(contract_address, *ret_vals.at(0))
}
#[test]
fn test_increase_balance() {
let (_, ret_val) = deploy_contract("HelloStarknet");
// Verify that the constructor actually returned 33 as expected.
assert(ret_val == 33, 'Invalid return value.');
}
Para confirmar, ejecuta scarb test, la prueba debería pasar. En un artículo posterior veremos cómo desplegar un contrato directamente desde otro.
Constructores tipo Payable en Starknet
Aunque STRK se comporta como un token ERC-20, también sirve como el token nativo de tarifas (fee token) de Starknet. Sin embargo, Starknet no tiene un verdadero “token nativo” en el mismo sentido que el ETH de Ethereum. Como resultado, Cairo no soporta constructores “payable” (pagaderos). Si queremos asegurar que un contrato tenga un cierto saldo de STRK al momento del despliegue, podemos transferir STRK a la dirección predicha, y luego comprobar mediante un assert en el constructor que el saldo del contrato es al menos la cantidad deseada.
Este artículo es parte de una serie de tutoriales sobre Cairo Programming on Starknet