Un contrato fábrica (factory contract) es un contrato que despliega una o más instancias de un contrato.
En el capítulo “Understanding Starknet’s Contract Deployment Model”, aprendimos que con el modelo declare-deploy de Starknet, primero debes declarar una clase de contrato una vez y luego puedes desplegar múltiples instancias a partir de ella manualmente. Sin embargo, cuando desplegamos instancias de contratos manualmente, debemos buscar el class hash correcto y pasar los argumentos adecuados del constructor (si los hay) cada vez.
Los contratos fábrica resuelven esto proporcionando una interfaz de despliegue consistente. En lugar de manejar los class hashes manualmente, simplemente llamas a la fábrica, la cual despliega una nueva instancia de contrato del contrato deseado:

En Ethereum (y otras cadenas EVM), el contrato fábrica utiliza el código de operación CREATE o CREATE2 internamente para desplegar nuevos contratos hijos. En Starknet, las fábricas logran el mismo comportamiento a través del deploy_syscall.
En este artículo, aprenderás a implementar el contrato fábrica utilizando deploy_syscall.
Cómo las fábricas usan deploy_syscall
Los contratos fábrica llaman a deploy_syscall directamente para desplegar instancias de contratos. Aquí está la firma de la función deploy_syscall:
pub extern fn deploy_syscall(
class_hash: ClassHash,
contract_address_salt: felt252,
calldata: Span<felt252>,
deploy_from_zero: bool
) -> Result<(ContractAddress, Span<felt252>), Array<felt252>>
implicits(GasBuiltin, System) nopanic;
Toma cuatro parámetros:
class_hash: el class hash del contrato que deseas desplegarcontract_address_salt: un valor salt para el cálculo de la direccióncalldata: argumentos del constructor para el nuevo contratodeploy_from_zero: determina si la dirección del desplegador se excluye del cálculo de la dirección del contrato. Cuando esfalse, la dirección del desplegador se incluye. Cuando estrue, se excluye y la dirección se calcula como si se hubiera desplegado desde la dirección0. Ten en cuenta que esto es el inverso del parámetronot_from_zeroen la interfaz del UDC.
El deploy_syscall devuelve un tipo Result con dos posibles resultados:
- En caso de éxito, obtienes la
ContractAddressdel contrato recién desplegado y unSpan<felt252>de datos de retorno serializados del constructor. Dado que los constructores en Cairo normalmente no devuelven valores, este span suele estar vacío, pero está disponible si el constructor devuelve valores explícitamente. - En caso de error, obtienes un
Array<felt252>que contiene información del error describiendo qué salió mal durante el despliegue
Nota:
implicits(GasBuiltin, System)son parámetros implícitos para el seguimiento del gas y operaciones del sistema, manejados automáticamente por Cairo.nopanicsignifica que la función devuelve un tipoResulten lugar de entrar en pánico (panic) ante los errores.
En un contrato fábrica, llamas a una función de fábrica como createContract(), que llama internamente a deploy_syscall con los parámetros requeridos: class_hash, salt, calldata y deploy_from_zero, como se muestra en la imagen a continuación:

Durante la ejecución de deploy_syscall, la red calcula la dirección del contrato usando la fórmula de dirección de contrato basada en Pedersen cubierta en Understanding Starknet’s Contract Deployment Model. El parámetro deploy_from_zero determina el valor deployer_address en esa fórmula: cuando es false, se establece en la dirección del contrato fábrica; cuando es true, se establece en 0.
La red luego despliega la instancia del contrato en esa dirección calculada y ejecuta el constructor con el calldata proporcionado.
Una vez que se completa el despliegue, deploy_syscall devuelve la nueva ContractAddress junto con cualquier dato de retorno del constructor a la fábrica. Si la implementación de la fábrica incluye un evento, emite datos relevantes del despliegue en este punto antes de devolver la ContractAddress al llamador original.
Universal Deployer Contract vs Contrato Fábrica Personalizado
El Universal Deployer Contract (UDC) que usamos para el despliegue regular de contratos es en sí mismo un contrato fábrica. Envuelve el deploy_syscall de bajo nivel y expone una interfaz simple para desplegar cualquier contrato declarado. Sin embargo, el UDC no rastrea los contratos desplegados y es estrictamente para propósitos de despliegue general.
Cuando necesitas más que un despliegue básico, construyes un contrato fábrica personalizado. Por ejemplo, es posible que desees rastrear qué contratos se desplegaron y por quién (registro), o restringir quién puede desplegar o actualizar las clases de contratos (control de acceso). En la siguiente sección, implementaremos una fábrica de tokens para mostrar cómo funcionan estas ideas.
Como cualquier contrato regular, el contrato fábrica se despliega a través del UDC. Pero cuando la fábrica despliega instancias, llama a
deploy_syscalldirectamente en lugar de pasar nuevamente por el UDC.
Creación de un Contrato Fábrica de Tokens
Construiremos un contrato fábrica ERC-20 que abstrae los class hashes y los parámetros de despliegue, permitiéndote desplegar instancias de tokens a través de una interfaz simple.
Actualizando el Contrato del Token
Necesitamos modificar nuestro contrato ERC-20 del capítulo “ERC-20 Token on Starknet”. La versión original tenía valores fijos (hardcoded) para el name, symbol y decimals del token. Dado que la fábrica necesita desplegar tokens con diferentes nombres y símbolos, estos valores deben ser configurables:
#[constructor]
fn constructor(
ref self: ContractState, token_name: ByteArray, symbol: ByteArray, owner: ContractAddress,
) {
self.token_name.write(token_name); //newly added
self.symbol.write(symbol); // newly added
self.decimal.write(18);
self.owner.write(owner);
}
token_name y symbol ahora son parámetros en lugar de valores fijos, owner se pasa como un parámetro para determinar quién puede acuñar (mint) tokens, y los decimales permanecen fijos en 18 (el estándar ERC-20).
Crea src/erc20.cairo en tu proyecto Scarb y pega el código completo actualizado del contrato en él:
use starknet::ContractAddress;
#[starknet::interface]
pub trait IERC20<TContractState> {
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;
fn name(self: @TContractState) -> ByteArray;
fn symbol(self: @TContractState) -> ByteArray;
fn decimals(self: @TContractState) -> u8;
fn mint(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
}
#[starknet::contract]
pub mod ERC20Token {
use starknet::storage::{
Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_caller_address};
#[storage]
pub struct Storage {
balances: Map<ContractAddress, u256>,
allowances: Map<
(ContractAddress, ContractAddress), u256,
>, // (owner, spender) -> amount, amount>
token_name: ByteArray,
symbol: ByteArray,
decimal: u8,
total_supply: u256,
owner: ContractAddress,
}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
Transfer: Transfer,
Approval: Approval,
}
#[derive(Drop, starknet::Event)]
pub struct Transfer {
#[key]
from: ContractAddress,
#[key]
to: ContractAddress,
amount: u256,
}
#[derive(Drop, starknet::Event)]
pub struct Approval {
#[key]
owner: ContractAddress,
#[key]
spender: ContractAddress,
value: u256,
}
#[constructor]
fn constructor(
ref self: ContractState, token_name: ByteArray, symbol: ByteArray, owner: ContractAddress,
) {
self.token_name.write(token_name);
self.symbol.write(symbol);
self.decimal.write(18);
self.owner.write(owner);
}
#[abi(embed_v0)]
impl ERC20Impl of super::IERC20<ContractState> {
fn total_supply(self: @ContractState) -> u256 {
self.total_supply.read()
}
fn balance_of(self: @ContractState, account: ContractAddress) -> u256 {
let balance = self.balances.entry(account).read();
balance
}
fn name(self: @ContractState) -> ByteArray {
self.token_name.read()
}
fn symbol(self: @ContractState) -> ByteArray {
self.symbol.read()
}
fn decimals(self: @ContractState) -> u8 {
self.decimal.read()
}
fn allowance(
self: @ContractState, owner: ContractAddress, spender: ContractAddress,
) -> u256 {
let allowance = self.allowances.entry((owner, spender)).read();
allowance
}
fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool {
let sender = get_caller_address();
let sender_prev_balance = self.balances.entry(sender).read();
let recipient_prev_balance = self.balances.entry(recipient).read();
assert(sender_prev_balance >= amount, 'Insufficient amount');
self.balances.entry(sender).write(sender_prev_balance - amount);
self.balances.entry(recipient).write(recipient_prev_balance + amount);
assert(
self.balances.entry(recipient).read() > recipient_prev_balance,
'Transaction failed',
);
self.emit(Transfer { from: sender, to: recipient, amount });
true
}
fn transfer_from(
ref self: ContractState,
sender: ContractAddress,
recipient: ContractAddress,
amount: u256,
) -> bool {
let spender = get_caller_address();
let spender_allowance = self.allowances.entry((sender, spender)).read();
let sender_balance = self.balances.entry(sender).read();
let recipient_balance = self.balances.entry(recipient).read();
assert(amount <= spender_allowance, 'amount exceeds allowance');
assert(amount <= sender_balance, 'amount exceeds balance');
self.allowances.entry((sender, spender)).write(spender_allowance - amount);
self.balances.entry(sender).write(sender_balance - amount);
self.balances.entry(recipient).write(recipient_balance + amount);
self.emit(Transfer { from: sender, to: recipient, amount });
true
}
fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool {
let caller = get_caller_address();
self.allowances.entry((caller, spender)).write(amount);
self.emit(Approval { owner: caller, spender, value: amount });
true
}
fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool {
let caller = get_caller_address();
assert(caller == self.owner.read(), 'Caller is not owner');
let previous_total_supply = self.total_supply.read();
let previous_balance = self.balances.entry(recipient).read();
self.total_supply.write(previous_total_supply + amount);
self.balances.entry(recipient).write(previous_balance + amount);
let zero_address: ContractAddress = 0.try_into().unwrap();
self.emit(Transfer { from: zero_address, to: recipient, amount });
true
}
}
}
Definiendo la Interfaz de la Fábrica (IERC20Factory)
Antes de escribir cualquier código de implementación, definamos qué hará nuestra fábrica. Nuestro contrato fábrica tendrá cuatro funciones principales:
- Desplegar un token: crea una nueva instancia de token con un nombre, símbolo y dirección del propietario.
- Desplegar un token en una dirección específica: crea un nuevo token con un salt especificado por el usuario, permitiendo al llamador determinar la dirección resultante del contrato.
- Consultar tokens desplegados: recupera todos los tokens desplegados a través de esta fábrica, ya sea globalmente o filtrados por usuario.
- Actualizar la clase del contrato del token: cambia la clase de contrato ERC-20 utilizada para todos los futuros despliegues de tokens.
use starknet::{ContractAddress, ClassHash};
#[starknet::interface]
pub trait IERC20Factory<TContractState> {
// Deploy a new ERC20 token contract with a user-specified salt
fn create_token_at(
ref self: TContractState,
salt: felt252,
name: ByteArray,
symbol: ByteArray,
owner: ContractAddress,
) -> ContractAddress;
// Deploy a new ERC20 token contract using a default salt
fn create_token(
ref self: TContractState,
name: ByteArray,
symbol: ByteArray,
owner: ContractAddress,
) -> ContractAddress;
// Update the stored class hash used for new ERC20 deployments
fn update_erc20_class_hash(ref self: TContractState, new_class_hash: ClassHash);
// Returns an array of all token contract addresses created by this factory
fn get_all_created_tokens(self: @TContractState) -> Array<ContractAddress>;
// Returns all token contract addresses created by a specific user
fn get_user_tokens(self: @TContractState, user: ContractAddress) -> Array<ContractAddress>;
}
Variables de Storage
A partir de la interfaz anterior, podemos determinar qué necesita almacenar el contrato fábrica. Necesitamos almacenar el class hash del token a partir del cual desplegaremos, rastrear cada token que creamos, organizarlos por creador y restringir quién puede actualizar el class hash del token utilizado para despliegues futuros. Esto nos lleva a definir estas variables de estado:
#[storage]
struct Storage {
token_class_hash: ClassHash, // class hash of the token contract to deploy
created_tokens: Vec<ContractAddress>, // global list of all deployed token instances
user_tokens: Map<ContractAddress, Vec<ContractAddress>>, // tokens deployed per user
factory_owner: ContractAddress, // address with admin rights over the factory
}
El token_class_hash almacena la clase de contrato ERC-20 que la fábrica utiliza para desplegar nuevos tokens. Cada token creado a través de esta fábrica será una instancia de este class hash. El propietario de la fábrica puede actualizar este valor más tarde para desplegar versiones mejoradas del token.
Mantenemos dos listas diferentes para los tokens creados:
- El vector
created_tokensproporciona un registro completo de cada token desplegado a través de esta fábrica. - Por su parte, el mapeo
user_tokenscrea listas individuales para cada usuario, almacenando únicamente los tokens que esa dirección específica ha creado.
El factory_owner almacena la dirección que tiene permiso para actualizar el class hash del token.
Constructor de la Fábrica
El constructor de la fábrica almacena el class hash del ERC-20 a partir del cual se desplegará y la dirección del propietario que tiene derechos de administrador sobre la fábrica:
#[constructor]
fn constructor(ref self: ContractState, token_class_hash: ClassHash, owner: ContractAddress) {
self.token_class_hash.write(token_class_hash);
self.factory_owner.write(owner);
}
Definiciones de Eventos
Necesitamos una forma para que las aplicaciones externas rastreen los despliegues y los cambios de class hash. Definiremos dos eventos que transmiten nuestras actividades clave:
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
TokenContractCreated: TokenContractCreated, // Emitted when a new token is created
ClassHashUpdated: ClassHashUpdated, // Emitted when the ERC20 class hash is updated
}
#[derive(Drop, starknet::Event)]
struct TokenContractCreated {
#[key]
creator: ContractAddress, // User who created the token
#[key]
token_address: ContractAddress, // Address of the new token contract
name: ByteArray,
symbol: ByteArray,
}
#[derive(Drop, starknet::Event)]
struct ClassHashUpdated {
old_class_hash: ClassHash,
new_class_hash: ClassHash,
}
Cuando se despliega un token, TokenContractCreated captura al creador, la dirección del token, el nombre y el símbolo, con campos indexados (#[key]) para una búsqueda eficiente. Cuando la plantilla del token (class hash) cambia, ClassHashUpdated registra la transición del antiguo al nuevo class hash.
Implementación del Contrato Fábrica
Ahora implementemos las funciones definidas en la interfaz de la fábrica. La fábrica proporciona dos formas de crear tokens, dependiendo de si los usuarios necesitan especificar su propio valor salt o no:
1. Creación de token con salt personalizado
La función create_token_at se utiliza para desplegar tokens con un salt especificado por el llamador, el cual determina la dirección resultante del contrato:
fn create_token_at(
ref self: ContractState,
salt: felt252,
name: ByteArray,
symbol: ByteArray,
owner: ContractAddress,
) -> ContractAddress {
let caller = get_caller_address();
let erc20_class_hash = self.token_class_hash.read();
// prepare constructor arguments
let mut constructor_calldata = array![];
name.serialize(ref constructor_calldata);
symbol.serialize(ref constructor_calldata);
owner.serialize(ref constructor_calldata);
// deploy the token contract
let (token_address, _) = deploy_syscall(
erc20_class_hash, salt, constructor_calldata.span(), false // deploy_from_zero
)
.unwrap_syscall();
// add to the global token list
self.created_tokens.push(token_address);
// append to user's token list
let user_tokens_entry = self.user_tokens.entry(caller);
user_tokens_entry.push(token_address);
// emit event
self
.emit(
TokenContractCreated {
creator: caller, token_address, name: name.clone(), symbol: symbol.clone(),
},
);
token_address
}
La función comienza obteniendo la dirección del llamador y leyendo el class hash del ERC-20 desde el almacenamiento, luego serializa los argumentos del constructor en un array. Cairo espera estos como un array de valores felt252 que la CairoVM pueda entender:
let mut constructor_calldata = array![];
name.serialize(ref constructor_calldata);
symbol.serialize(ref constructor_calldata);
owner.serialize(ref constructor_calldata);
Así que la función toma el nombre, el símbolo y la dirección del propietario del token y los serializa en el orden exacto que coincide con el constructor del contrato del token. Equivocarse en este orden causaría que el despliegue falle porque el constructor recibiría tipos de argumentos no coincidentes.
Una vez que los datos están correctamente serializados, la función llama a deploy_syscall con el class hash, el salt y los argumentos serializados del constructor.
// deploy the token contract
let (token_address, _) = deploy_syscall(
erc20_class_hash, salt, constructor_calldata.span(), false // deploy_from_zero
)
.unwrap_syscall();
Como se mencionó en la sección de parámetros de deploy_syscall, pasar false en deploy_from_zero le indica a Starknet que despliegue desde la dirección de la fábrica en lugar de cero, lo que afecta cómo se calcula la dirección final del contrato.
Después de que el despliegue tiene éxito, la fábrica lleva un registro del nuevo token en dos lugares: una lista global de todos los tokens creados created_tokens y una lista personal para cada usuario user_tokens. También emite un evento TokenContractCreated, y luego devuelve la nueva dirección del contrato a quienquiera que haya llamado a la función.
2. Creación de token con salt por defecto
La función create_token proporciona la forma más sencilla de desplegar un nuevo token. Toma tres parámetros: el name, el symbol del token y la dirección del owner, y delega el despliegue real a create_token_at con un salt por defecto de 0.
fn create_token(
ref self: ContractState,
name: ByteArray,
symbol: ByteArray,
owner: ContractAddress,
) -> ContractAddress {
// Use default salt of 0
self.create_token_at(0, name, symbol, owner)
}
Usar 0 como salt por defecto es una convención común, aunque cualquier valor salt fijo funciona si estás implementando tu propia fábrica.
¿Por qué usar un salt por defecto?
- Los usuarios no necesitan entender o administrar los valores de salt.
- Cada despliegue obtiene automáticamente una dirección única siempre y cuando los parámetros sean diferentes; de lo contrario, obtendrás la misma dirección (aunque el segundo despliegue fallaría ya que la dirección ya estaría ocupada).
Funciones de Registro y Consulta
La función get_all_created_tokens devuelve cada token que haya sido desplegado por esta fábrica:
fn get_all_created_tokens(self: @ContractState) -> Array<ContractAddress> {
let mut tokens = array![];
for i in 0..self.created_tokens.len() {
tokens.append(self.created_tokens.at(i).read());
}
tokens
}
Itera a través del vector de almacenamiento created_tokens, añade cada dirección a un array tokens, y lo devuelve.
La función get_user_tokens funciona de manera similar pero se centra en los tokens creados por un usuario específico:
fn get_user_tokens(self: @ContractState, user: ContractAddress) -> Array<ContractAddress> {
let user_tokens_entry = self.user_tokens.entry(user);
let mut tokens = array![];
for i in 0..user_tokens_entry.len() {
tokens.append(user_tokens_entry.at(i).read());
}
tokens
}
Toma la dirección de un usuario como entrada, busca la entrada de ese usuario en el mapeo user_tokens, y luego itera a través de su lista personal de tokens para construir el array de retorno. Esto es particularmente útil para interfaces de wallets o rastreadores de portafolios donde los usuarios solo desean ver los tokens que han creado.
Ambas funciones usan el mismo patrón: crean un array mutable, iteran a través de la estructura de almacenamiento relevante, y añaden cada dirección que encuentran. La principal diferencia es que una lee del registro global mientras que la otra lee de un mapeo específico de usuario.
Función de actualización de class hash
La fábrica utiliza la función update_erc20_class_hash para actualizar la clase del contrato del token que usa para los nuevos despliegues:
fn update_erc20_class_hash(ref self: ContractState, new_class_hash: ClassHash) {
let caller = get_caller_address();
assert(caller == self.factory_owner.read(), 'Only owner can update');
let old_class_hash = self.token_class_hash.read();
self.token_class_hash.write(new_class_hash);
self.emit(ClassHashUpdated { old_class_hash, new_class_hash });
}
Al ser llamada, la función verifica que el llamador sea el propietario de la fábrica. De no ser así, la transacción falla con un error de “Only owner can update”.
Una vez que pasa la verificación de permisos, la función lee el class hash actual y lo almacena como el valor antiguo, luego escribe el nuevo class hash en el almacenamiento. Esto significa que todos los despliegues de tokens futuros usarán la implementación actualizada del contrato, mientras que los tokens existentes permanecerán sin cambios en su versión original. La función entonces emite un evento ClassHashUpdated que contiene ambos valores de class hash.
Ten en cuenta que la actualización del class hash solo funcionará si el nuevo class hash del contrato sigue la misma firma de constructor y orden de serialización que el original. Actualmente, la función create_token_at de la fábrica espera:
fn create_token_at(
ref self: ContractState,
salt: felt252,
name: ByteArray,
symbol: ByteArray,
owner: ContractAddress,
) -> ContractAddress
Cualquier class hash actualizado debe tener un constructor que contenga exactamente estos parámetros en el mismo orden.
let mut constructor_calldata = array![];
name.serialize(ref constructor_calldata);
symbol.serialize(ref constructor_calldata);
owner.serialize(ref constructor_calldata);
Si el nuevo contrato tiene diferentes parámetros en el constructor, la creación del token fallará debido a desajustes de serialización durante la inicialización.
Antes de desplegar y probar la fábrica, copia y pega el contrato completo de la fábrica ERC20 en src/erc20_factory.cairo:
use starknet::{ClassHash, ContractAddress};
#[starknet::interface]
pub trait IERC20Factory<TContractState> {
fn create_token(
ref self: TContractState, name: ByteArray, symbol: ByteArray, owner: ContractAddress,
) -> ContractAddress;
fn create_token_at(
ref self: TContractState,
salt: felt252,
name: ByteArray,
symbol: ByteArray,
owner: ContractAddress,
) -> ContractAddress;
fn update_erc20_class_hash(ref self: TContractState, new_class_hash: ClassHash);
fn get_all_created_tokens(self: @TContractState) -> Array<ContractAddress>;
fn get_user_tokens(self: @TContractState, user: ContractAddress) -> Array<ContractAddress>;
}
#[starknet::contract]
mod ERC20TokenFactory {
use starknet::storage::{
Map, MutableVecTrait, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess,
Vec, VecTrait,
};
use starknet::syscalls::deploy_syscall;
use starknet::{ClassHash, ContractAddress, SyscallResultTrait, get_caller_address};
#[storage]
struct Storage {
token_class_hash: ClassHash,
created_tokens: Vec<ContractAddress>,
user_tokens: Map<ContractAddress, Vec<ContractAddress>>,
factory_owner: ContractAddress,
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
TokenContractCreated: TokenContractCreated,
ClassHashUpdated: ClassHashUpdated,
}
#[derive(Drop, starknet::Event)]
struct TokenContractCreated {
#[key]
creator: ContractAddress,
#[key]
token_address: ContractAddress,
name: ByteArray,
symbol: ByteArray,
}
#[derive(Drop, starknet::Event)]
struct ClassHashUpdated {
old_class_hash: ClassHash,
new_class_hash: ClassHash,
}
#[constructor]
fn constructor(ref self: ContractState, token_class_hash: ClassHash, owner: ContractAddress) {
self.token_class_hash.write(token_class_hash);
self.factory_owner.write(owner);
}
#[abi(embed_v0)]
impl ERC20FactoryImpl of super::IERC20Factory<ContractState> {
fn create_token(
ref self: ContractState, name: ByteArray, symbol: ByteArray, owner: ContractAddress,
) -> ContractAddress {
// Use default salt of 0
self.create_token_at(0, name, symbol, owner)
}
fn create_token_at(
ref self: ContractState,
salt: felt252,
name: ByteArray,
symbol: ByteArray,
owner: ContractAddress,
) -> ContractAddress {
let caller = get_caller_address();
let erc20_class_hash = self.token_class_hash.read();
// prepare constructor arguments
let mut constructor_calldata = array![];
name.serialize(ref constructor_calldata);
symbol.serialize(ref constructor_calldata);
owner.serialize(ref constructor_calldata);
// deploy the token contract
let (token_address, _) = deploy_syscall(
erc20_class_hash, salt, constructor_calldata.span(), false // deploy_from_zero
)
.unwrap_syscall();
// track the created token
self.created_tokens.push(token_address);
// track user's tokens
let user_tokens_entry = self.user_tokens.entry(caller);
user_tokens_entry.push(token_address);
// emit event
self
.emit(
TokenContractCreated {
creator: caller, token_address, name: name.clone(), symbol: symbol.clone(),
},
);
token_address
}
fn update_erc20_class_hash(ref self: ContractState, new_class_hash: ClassHash) {
let caller = get_caller_address();
assert(caller == self.factory_owner.read(), 'Only owner can update');
let old_class_hash = self.token_class_hash.read();
self.token_class_hash.write(new_class_hash);
self.emit(ClassHashUpdated { old_class_hash, new_class_hash });
}
fn get_all_created_tokens(self: @ContractState) -> Array<ContractAddress> {
let mut tokens = array![];
for i in 0..self.created_tokens.len() {
tokens.append(self.created_tokens.at(i).read());
}
tokens
}
fn get_user_tokens(self: @ContractState, user: ContractAddress) -> Array<ContractAddress> {
let user_tokens_entry = self.user_tokens.entry(user);
let mut tokens = array![];
for i in 0..user_tokens_entry.len() {
tokens.append(user_tokens_entry.at(i).read());
}
tokens
}
}
}
En el archivo src/lib.cairo, ambos contratos deben ser declarados como módulos públicos para que puedan ser accedidos por el compilador de Cairo:
pub mod erc20;
pub mod erc20_factory;
El archivo lib.cairo sirve como punto de entrada para el proyecto Cairo. Le dice al compilador qué módulos incluir en la compilación. Declarar ambos módulos como pub los hace accesibles a otros módulos en el proyecto.
La estructura final del proyecto debería verse de la siguiente manera:
src/
├── lib.cairo
└── erc20.cairo
└── erc20_factory.cairo
Desplegando la Fábrica usando sncast
Ahora que tenemos ambos contratos escritos, vamos a declararlos para obtener sus class hashes, y luego desplegar el contrato fábrica, el cual usará el class hash del ERC-20 para crear tokens más adelante.
Paso 1: Declarar el Contrato del Token ERC-20
Primero, necesitamos declarar nuestro contrato del token para registrarlo en Starknet y obtener su class hash:
sncast \
--account <ACCOUNT_NAME> \
declare \
--url https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/<YOUR_API_KEY> \
--contract-name ERC20Token
Reemplaza ACCOUNT_NAME con el nombre real de la cuenta y tu YOUR_API_KEY con tu API Key de Alchemy, luego ejecuta el comando. Verás un resultado similar a este:

Necesitamos guardar este class hash, será requerido para el despliegue de la fábrica.
Paso 2: Declarar el Contrato Fábrica
A continuación, ejecutaremos el siguiente comando de sncast para declarar nuestro contrato fábrica:
sncast \
--account <ACCOUNT_NAME> \
declare \
--url https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/<YOUR_API_KEY> \
--contract-name ERC20TokenFactory
Esto devolverá el class hash de nuestro contrato fábrica:

Paso 3: Desplegar el Contrato Fábrica
Ahora desplegamos nuestro contrato fábrica usando su class hash:
sncast \
--account <ACCOUNT_NAME> \
deploy \
--class-hash <ERC20TOKENFACTORY_CLASS_HASH> \
--arguments '<ERC20_CLASS_HASH>, <OWNER_ADDRESS>' \
--url https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/<YOUR_API_KEY>
El calldata del constructor contiene dos argumentos:
- Class hash del ERC-20 (
0xea2b282ed...): le dice a la fábrica qué clase de contrato usar. - Dirección del propietario de la fábrica (
0x014154fb...): establece quién puede actualizar el class hash de la fábrica.
Después de un despliegue exitoso, obtendremos una dirección de contrato:

Verificando el Despliegue de la Fábrica vía UDC
Con nuestro contrato fábrica desplegado, examinemos cómo se desplegó realmente para confirmar nuestra explicación anterior en la sección “Universal Deployer Contract vs Contrato Fábrica Personalizado”.
En Voyager, busca la transacción utilizando el hash de la transacción (no la dirección del contrato).
Al observar las llamadas internas (haz clic en deployContract y luego en “More Details”), podemos verificar que el propio contrato fábrica fue desplegado a través del Universal Deployer Contract (UDC):

- Llamada de despliegue del UDC: El UDC (resaltado en la caja roja en la parte superior) recibe la solicitud de despliegue de la fábrica y llama a su función
deployContractcon los parámetros necesarios. - Class hash de la fábrica: El parámetro
classHash(resaltado en amarillo) muestra0x1843d25804e7cc40c7b77d415b96d2316a6176a3e0ff454bb5a529d1696990a- este es el class hash de nuestro contrato fábrica que fue declarado previamente. - Salt: El parámetro
saltmuestra0x8bab3046b4b8227generado porsncastpara asegurar direcciones de contrato únicas. - Parámetros del constructor: En el array
calldata, podemos ver:- Primer parámetro (
0x2466cbc06f94c3e7b9a95bfc7ef94295f1546fa1917ded31710510b30d58e3d): Este es el class hash del contrato del token ERC-20 que nuestra fábrica usará para desplegar tokens. - Segundo parámetro (
0x14154fb6dd088b5ceb46df635ecce6e1a9b0455357931ac7df4263a7dbf39a9): Esta es la dirección que pasamos como propietario de la fábrica.
- Primer parámetro (
- Resultado del despliegue: La salida muestra la dirección del contrato fábrica desplegado:
0xcdcb37a51075426d2b91ff49769f6909b51e8384ae1aae8dd2b82fdb923e4d.
El contrato fábrica usa deploy_syscall directamente para crear instancias de tokens individuales, eludiendo la necesidad de pasar por el UDC para cada despliegue de token.
Usando la Fábrica para Crear Tokens
Veamos cómo los usuarios pueden interactuar con nuestra fábrica desplegada para crear sus propios tokens. Usaremos la interfaz de Voyager para mostrar el proceso de creación del token.
Creando un token a través de la fábrica
Cuando navegamos a nuestro contrato fábrica en Voyager en 0xcdcb37a51075426d2b91ff49769f6909b51e8384ae1aae8dd2b82fdb923e4d, y vamos a la pestaña Write Contract, podemos ver las funciones de escritura disponibles. La primera que tenemos es la interfaz de la función create_token donde los usuarios pueden ingresar los parámetros de su token.
Para crear un token, rellenamos los campos obligatorios:
- name: “SarcToken” (el nombre completo de nuestro token)
- symbol: “SRC” (el símbolo de trading)
- owner: La dirección que poseerá y controlará el token

Después de conectar nuestra wallet y hacer clic en “Transact”, la fábrica despliega un nuevo contrato de token ERC-20. La transacción devuelve la dirección de nuestro token recién creado: 0x849daeb52f488856b408df096efcb3cba66243373a5ecb6bd62c1abb7c51d9.
Verificando el Token Creado
Ahora podemos interactuar con nuestro token recién creado en la dirección de su contrato. Ve a la pestaña Read Contract para verificar los detalles del token. El token implementa la funcionalidad estándar ERC-20; los usuarios pueden acuñar tokens (si son los propietarios), transferirlos, comprobar saldos, y aprobar asignaciones de gasto.
Llamar a get_all_created_tokens puede recuperar un array de todos los contratos de tokens que se han desplegado a través de esta fábrica:

Actualizando el Class hash del Token
Al actualizar el class hash a través de update_erc20_class_hash, la fábrica usará la nueva plantilla para los siguientes despliegues de tokens. Los tokens desplegados previamente antes de la actualización permanecen sin cambios con su implementación existente (el antiguo class hash).
Nuevamente, antes de actualizar el class hash, asegúrate de que la nueva clase del contrato tenga exactamente la misma firma del constructor y el mismo orden de serialización que la actual. Si no coinciden, la fábrica pasará parámetros incorrectos en el constructor al nuevo contrato, causando que el despliegue falle.

Conclusión
Todos los despliegues en Starknet usan en última instancia la función deploy_syscall, pero las fábricas envuelven esta funcionalidad de bajo nivel en interfaces simples. Nuestra fábrica ERC-20 muestra cómo un contrato puede manejar la creación de tokens para múltiples usuarios mientras mantiene un registro de lo que se ha desplegado.
La función update_erc20_class_hash funciona bien para actualizar a nuevas versiones de tokens que utilizan los mismos parámetros de constructor. Para situaciones en las que deseas que la fábrica cree tokens con diferentes requisitos de inicialización o serialización, puedes actualizar la propia fábrica usando replace_class_syscall, lo cual se discutirá en otro artículo. De manera similar, los tokens ERC20 individuales pueden incluir funcionalidad de actualización para permitir a sus propietarios actualizar la lógica del token cuando sea necesario.