En Ethereum, el patrón proxy es el enfoque más común para la actualización de contratos. En este patrón, un contrato proxy mantiene el almacenamiento (storage) del contrato y delega las llamadas a funciones a un contrato de implementación separado. Cuando se necesita una actualización, se despliega un nuevo contrato de implementación y se apunta el proxy hacia él.
Starknet adopta un enfoque diferente, utiliza el replace_class_syscall para intercambiar el class hash de un contrato desplegado sin cambiar el almacenamiento ni la dirección (address). Este artículo cubre cómo funciona replace_class_syscall y cómo implementar actualizaciones de contratos en Starknet.
Cómo funciona replace_class_syscall
Recuerde del capítulo sobre “Understanding Starknet’s Contract Deployment Model” que cada contrato es una instancia de una clase de contrato (contract class): la clase contiene el bytecode, y la instancia mantiene el almacenamiento con su dirección. Debido a que el bytecode y el almacenamiento viven por separado, puede apuntar la instancia del contrato a una nueva clase mientras conserva el almacenamiento.
Para actualizar un contrato usando replace_class_syscall, pasamos el class hash de la nueva implementación como argumento (new_class_hash: ClassHash).
La firma de la función replace_class_syscall es:
fn replace_class_syscall(new_class_hash: ClassHash) -> SyscallResult<()>
Retorna SyscallResult<()>: un tipo de resultado que envuelve Ok(()) en caso de éxito o Err con los detalles del error en caso de fallo. El syscall falla cuando el class hash no ha sido declarado en Starknet o cuando el llamador (caller) no tiene permiso para realizar la actualización.
Cuando replace_class_syscall se ejecuta con éxito:
- La dirección del contrato permanece igual (independientemente de cuántas actualizaciones ocurran)
- Todos los datos de almacenamiento permanecen en el propio almacenamiento del contrato
- La instancia del contrato comienza a usar la lógica del nuevo class hash una vez que la ejecución retorna al llamador.
Ahora que entendemos cómo funciona replace_class_syscall, usémoslo para actualizar un contrato.
Actualizaciones de Contratos con replace_class_syscall
Actualizaremos un contrato Counter que incrementa un conteo en 1 y recupera el conteo actual a una nueva implementación de clase, el contrato Greeter.
Contrato Counter
Para seguir los pasos, cree un nuevo proyecto y navegue hacia él:
scarb new counter_upgrade
cd counter_upgrade
Luego cree un archivo src/counter.cairo y agregue el código a continuación.
Nos centraremos en la implementación de la función upgrade.
use starknet::{ClassHash};
#[starknet::interface]
pub trait ICounter<TContractState> {
fn get_count(self: @TContractState) -> u32;
fn increment(ref self: TContractState);
fn upgrade(ref self: TContractState, new_class_hash: ClassHash);
}
#[starknet::contract]
mod Counter {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
use starknet::syscalls::replace_class_syscall;
use starknet::SyscallResultTrait;
use starknet::{ClassHash, ContractAddress, get_caller_address};
#[storage]
struct Storage {
count: u32,
owner: ContractAddress,
current_class_hash: ClassHash,
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
ContractUpgraded: ContractUpgraded,
}
#[derive(Drop, starknet::Event)]
struct ContractUpgraded {
old_class_hash: ClassHash, // Previous implementation class hash
new_class_hash: ClassHash, // New implementation class hash
}
#[constructor]
fn constructor(ref self: ContractState, owner: ContractAddress, initial_class_hash: ClassHash) {
self.count.write(1); // Initialize counter to 1
self.owner.write(owner); // Sets the contract owner
self.current_class_hash.write(initial_class_hash); // Just to track the class hashes
}
#[abi(embed_v0)]
impl CounterImpl of super::ICounter<ContractState> {
// returns the current counter value
fn get_count(self: @ContractState) -> u32 {
self.count.read()
}
// increments the counter by 1
fn increment(ref self: ContractState) {
let current = self.count.read();
self.count.write(current + 1);
}
// FOCUS HERE: upgrades the contract to use a new implementation
fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {
let caller = get_caller_address();
assert(caller == self.owner.read(), 'Only contract owner can upgrade');
let old_class_hash = self.current_class_hash.read();
replace_class_syscall(new_class_hash).unwrap_syscall();
self.current_class_hash.write(new_class_hash);
self.emit(ContractUpgraded { old_class_hash, new_class_hash });
}
}
}
La función upgrade acepta el nuevo class hash como argumento y asegura que el llamador sea el propietario del contrato para evitar actualizaciones no autorizadas.
let caller = get_caller_address();
assert(caller == self.owner.read(), 'Only contract owner can upgrade');
La actualización real ocurre en la línea:
replace_class_syscall(new_class_hash).unwrap_syscall();
unwrap_syscall() genera un panic si replace_class_syscall retorna un error, lo que causa que la transacción se revierta (revert). Esto significa que la actualización se completa con éxito o la transacción genera un panic, se revierte y deja el contrato sin cambios.
Una vez que la actualización se completa, la función emite un ContractUpgraded para registrar el cambio:
self.emit(ContractUpgraded { old_class_hash, new_class_hash });
Reemplace el contenido de src/lib.cairo con mod counter; para indicarle al compilador qué módulos incluir en la compilación.
Declaración y Despliegue del Contrato Counter
Ahora vamos a declarar y desplegar el contrato para probar la funcionalidad de actualización.
Ejecute el siguiente comando para declarar el contrato Counter. Reemplace YOUR_ACCOUNT_NAME con el nombre de su cuenta y YOUR_API_KEY con su clave de API de Alchemy:
sncast \
--account <YOUR_ACCOUNT_NAME> \
declare \
--url https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/<YOUR_API_KEY> \
--contract-name Counter

Desplegar el contrato Counter
Dado que el constructor de Counter espera el class hash inicial como argumento para rastrear las actualizaciones, le pasamos el class hash del paso de declaración anterior, junto con la dirección del propietario. Ejecute el siguiente comando para desplegar, reemplazando los marcadores de posición con sus valores concretos:
sncast \
--account <YOUR_ACCOUNT_NAME> \
deploy \
--class-hash <COUNTER_CLASS_HASH> \
--arguments '<OWNER_ADDRESS>, <COUNTER_CLASS_HASH>' \
--url https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/<YOUR_API_KEY>

Después del despliegue, el count debería ser 1 ya que lo inicializamos en 1 en el constructor. Podemos verificar esto llamando a get_count a través de la interfaz de lectura de contratos de Voyager, que retorna 1.

Para actualizar el contrato Counter, necesitamos un nuevo class hash. Obtenemos esto declarando un segundo contrato llamado Greeter.
Creando el Contrato Greeter
El contrato Greeter establecerá y recuperará mensajes de saludo, rastreará el conteo de saludos e incluirá funcionalidad de actualización. Estamos usando un contrato estructuralmente diferente en lugar de una versión más nueva de Counter para demostrar tres cosas:
- Cómo se conserva el almacenamiento a través de las actualizaciones
- Cómo se comportan las colisiones de nombres de campos
- Cuándo exactamente entra en vigor la nueva implementación después de llamar a
replace_class_syscall
Cada uno de estos puntos se cubre en las siguientes secciones.
Cree un archivo src/greeter.cairo en el mismo proyecto counter_upgrade y agregue el siguiente código:
use starknet::{ClassHash, ContractAddress};
#[starknet::interface]
pub trait IGreeter<TContractState> {
fn set_greeting(ref self: TContractState, message: ByteArray);
fn get_greeting(self: @TContractState) -> ByteArray;
fn get_greeting_count(self: @TContractState) -> u32;
fn upgrade(ref self: TContractState, new_class_hash: ClassHash);
fn get_owner(self: @TContractState) -> ContractAddress;
}
#[starknet::contract]
mod Greeter {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
use starknet::syscalls::replace_class_syscall;
use starknet::SyscallResultTrait;
use starknet::{ClassHash, ContractAddress, get_caller_address};
#[storage]
struct Storage {
greeting: ByteArray,
greeting_count: u32,
owner: ContractAddress,
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
ContractUpgraded: ContractUpgraded,
}
#[derive(Drop, starknet::Event)]
struct ContractUpgraded {
new_class_hash: ClassHash,
}
#[constructor]
fn constructor(ref self: ContractState, owner: ContractAddress) {
self.owner.write(owner); // Set the contract owner
}
#[abi(embed_v0)]
impl GreeterImpl of super::IGreeter<ContractState> {
// Updates the greeting message and increments the usage counter
fn set_greeting(ref self: ContractState, message: ByteArray) {
self.greeting.write(message);
let current_count = self.greeting_count.read();
let new_count = current_count + 1;
self.greeting_count.write(new_count);
}
// Returns the current greeting message
fn get_greeting(self: @ContractState) -> ByteArray {
self.greeting.read()
}
// Returns how many times the greeting has been updated
fn get_greeting_count(self: @ContractState) -> u32 {
self.greeting_count.read()
}
// Upgrades the contract to use a new implementation
fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {
let caller = get_caller_address();
assert(caller == self.owner.read(), 'Only owner can upgrade');
replace_class_syscall(new_class_hash).unwrap_syscall();
self.emit(ContractUpgraded { new_class_hash });
}
// Returns the contract owner's address
fn get_owner(self: @ContractState) -> ContractAddress {
self.owner.read()
}
}
}
Luego agregue mod greeter; a src/lib.cairo, para que tengamos lo siguiente en el lib.cairo:
mod counter;
mod greeter;
Declare el contrato Greeter para obtener el class hash para actualizar el contrato Counter:
sncast \
--account <YOUR_ACCOUNT_NAME> \
declare \
--url https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/<YOUR_API_KEY> \
--contract-name Greeter

Usando la dirección establecida como propietaria durante el despliegue del Counter, llame a la función upgrade en la pestaña “Write Contract” de Voyager con el class hash de Greeter como argumento y la transacción debería tener éxito:

Almacenamiento del Contrato Antes y Después de la Actualización
Antes de la actualización, el contrato Counter tenía este estado:
- count =
1 - owner =
0x014154fb6Dd088b5ceB46df635eCCe6e1a9B0455357931aC7Df4263A7dBf39a9 - current_class_hash =
0xd6574c9c64c779442f0f958db1935708b09d18a4cfb98a86e0ac1ded53ebd9
Después de la actualización, el contrato mantiene todos sus datos almacenados mientras ejecuta el código del class hash de Greeter:
- count =
1 - owner =
0x014154fb6Dd088b5ceB46df635eCCe6e1a9B0455357931aC7Df4263A7dBf39a9 - current_class_hash =
0x6cc6a96920706f49a5579a2c0f235e8480daa047500e4acc3910b5da0c010c0 - greeting =
0 - greeting_count =
0
Note que ambos contratos tienen un campo owner que se mapea a la misma ubicación de almacenamiento (calculada a partir de sn_keccak("owner")).
Podemos verificar esto usando Voyager’s storage query interface. La interfaz actual de Voyager solo permite consultar un slot de almacenamiento a la vez.
Para consultar todos los slots a la vez, haga clic en “View old version of this page” en la parte superior de la página del contrato, y seleccione “Struct” en el menú desplegable del tipo de consulta.

Luego pegue el siguiente struct en el campo de entrada. Note que este struct combina las variables de almacenamiento de las implementaciones de clase de Counter y Greeter:
#[storage]
struct Storage {
count: u32,
owner: ContractAddress,
current_class_hash: ClassHash,
greeting: ByteArray,
owner: ContractAddress,
}
Haga clic en “Query Struct Data” para ver cómo el mismo almacenamiento es interpretado a través de la estructura del contrato:

Note que el nuevo class hash se aplica a llamadas posteriores solo después de que finalice la llamada actual a la función upgrade.
Considere este código de ejemplo que muestra exactamente cuándo entra en vigor la actualización dentro de la función de actualización:
fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {
let count_before = self.count.read(); // count_before = 1
replace_class_syscall(new_class_hash).unwrap(); // Syscall succeeds
// But this STILL uses the OLD implementation!
self.increment(); // count goes from 1 to 2 (old logic)
let count_after = self.count.read(); // count_after = 2
}
- Mientras se ejecuta la llamada a
upgrade:count_before = 1, luego después deself.increment()usando la lógica antigua,count_after = 2 - Después de que se completa la función de actualización: las llamadas posteriores al contrato ejecutan código del nuevo class hash
Esto significa que replace_class_syscall registra el nuevo class hash, pero la llamada actual continúa ejecutando el código de la clase antigua. Si necesita ejecutar código de la nueva clase dentro de la misma transacción, combine replace_class_syscall con call_contract_syscall.
Uso de replace_class_syscall con call_contract_syscall para Actualizaciones
Al actualizar, siempre se llama primero a replace_class_syscall para registrar la nueva clase. Si luego necesita invocar inmediatamente la nueva implementación dentro de la misma función, debe seguirlo con call_contract_syscall.
Cualquier invocación a call_contract_syscall hacia el mismo contrato después de replace_class_syscall se ejecutará usando la nueva implementación, a pesar de que las llamadas directas a funciones dentro de la propia función de actualización aún se ejecutan bajo la implementación antigua.
Aquí está la misma función de actualización reescrita para usar call_contract_syscall en lugar de self.increment():
fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {
let count_before = self.count.read(); // count_before = 1
replace_class_syscall(new_class_hash).unwrap(); // Registers the new class
let increment_selector = selector!("increment");
call_contract_syscall( // Immediately executes using the NEW implementation
get_contract_address(),
increment_selector, // Dispatches the increment call
array![].span()
).unwrap();
let count_after = self.count.read(); // count_after = 1 (new increment does nothing)
}
A diferencia de self.increment() que continúa utilizando la implementación antigua durante la ejecución de la función de actualización, call_contract_syscall despacha la llamada de incremento a la nueva clase. Dado que replace_class_syscall ya ha actualizado el contrato para apuntar a esa nueva clase, call_contract_syscall ejecuta la nueva implementación. Esta es la razón por la que count_after permanece en 1 en lugar de incrementarse a 2.
Verifiquemos ambos enfoques de actualización on-chain con un contrato Counter actualizado que los implemente como funciones separadas.
Versión 1: replace_class_syscall Estándar
Probaremos si la actualización entra en vigor de inmediato verificando el conteo antes y después de llamar a increment() dentro de la misma función.
Comience actualizando el evento ContractUpgraded en Counter para incluir el seguimiento del conteo:
#[derive(Drop, starknet::Event)]
struct ContractUpgraded {
old_class_hash: ClassHash,
new_class_hash: ClassHash,
count_before: u32, //ADD THIS
count_after: u32, //ADD THIS
}
Reemplace la función upgrade por upgrade_standard:
fn upgrade_standard(ref self: ContractState, new_class_hash: ClassHash) {
let caller = get_caller_address();
assert(caller == self.owner.read(), 'Only owner can upgrade');
// Check count before upgrade
let count_before = self.count.read();
let old_class_hash = self.current_class_hash.read();
replace_class_syscall(new_class_hash).unwrap_syscall();
self.current_class_hash.write(new_class_hash);
// Test: Does this use old or new implementation?
self.increment();
// Check count after increment
let count_after = self.count.read();
self.emit(ContractUpgraded {
old_class_hash,
new_class_hash,
count_before,
count_after
});
}
Luego, actualice la interfaz para reflejar la función upgrade_standard:
#[starknet::interface]
pub trait ICounter<TContractState> {
fn get_count(self: @TContractState) -> u32;
fn increment(ref self: TContractState);
fn upgrade_standard(ref self: TContractState, new_class_hash: ClassHash); //ADD THIS
}
Aquí está el contrato Counter completamente actualizado con la función upgrade_standard:
use starknet::ClassHash;
#[starknet::interface]
pub trait ICounter<TContractState> {
fn get_count(self: @TContractState) -> u32;
fn increment(ref self: TContractState);
fn upgrade_standard(ref self: TContractState, new_class_hash: ClassHash);
}
#[starknet::contract]
mod Counter {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
use starknet::syscalls::replace_class_syscall;
use starknet::{ClassHash, ContractAddress, SyscallResultTrait, get_caller_address};
#[storage]
struct Storage {
count: u32,
owner: ContractAddress,
current_class_hash: ClassHash,
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
ContractUpgraded: ContractUpgraded
}
#[derive(Drop, starknet::Event)]
struct ContractUpgraded {
old_class_hash: ClassHash,
new_class_hash: ClassHash,
count_before: u32,
count_after: u32,
}
#[constructor]
fn constructor(ref self: ContractState, owner: ContractAddress, initial_class_hash: ClassHash) {
self.count.write(1); // Initialize counter to 1
self.owner.write(owner); // Sets the contract owner
self.current_class_hash.write(initial_class_hash); // to track the class hashes
}
#[abi(embed_v0)]
impl CounterImpl of super::ICounter<ContractState> {
// returns the current counter value
fn get_count(self: @ContractState) -> u32 {
self.count.read()
}
// increments the counter by 1
fn increment(ref self: ContractState) {
let current = self.count.read();
self.count.write(current + 1);
}
fn upgrade_standard(ref self: ContractState, new_class_hash: ClassHash) {
let caller = get_caller_address();
assert(caller == self.owner.read(), 'Only owner can upgrade');
// Check count before upgrade
let count_before = self.count.read();
let old_class_hash = self.current_class_hash.read();
replace_class_syscall(new_class_hash).unwrap_syscall();
self.current_class_hash.write(new_class_hash);
// Test: Does this use old or new implementation?
self.increment();
// Check count after increment
let count_after = self.count.read();
self
.emit(
ContractUpgraded { old_class_hash, new_class_hash, count_before, count_after },
);
}
}
}
Vuelva a declarar y desplegar el contrato Counter actualizado, luego llame a upgrade_standard con el class hash de Greeter a través de la pestaña Write Contract de Voyager.

El evento ContractUpgraded emitted (emitido) muestra que el conteo aumentó de 1 a 2, confirmando que la función increment() de la implementación antigua se utilizó durante la ejecución de la función de actualización:

Versión 2: Uso de call_contract_syscall dentro de la función de actualización
Ahora vamos a crear una versión de actualización que usa call_contract_syscall para confirmar si podemos acceder inmediatamente a la nueva implementación dentro de la misma transacción.
Reemplace la función upgrade_standard con la siguiente implementación de upgrade_with_call que usa call_contract_syscall:
fn upgrade_with_call(ref self: ContractState, new_class_hash: ClassHash) {
let caller = get_caller_address();
assert(caller == self.owner.read(), 'Only owner can upgrade');
// Check count before upgrade
let count_before = self.count.read();
let old_class_hash = self.current_class_hash.read();
replace_class_syscall(new_class_hash).unwrap_syscall();
self.current_class_hash.write(new_class_hash);
// Test: Call increment using call_contract_syscall to see if new implementation is used
let increment_selector = selector!("increment");
call_contract_syscall(get_contract_address(), increment_selector, array![].span())
.unwrap_syscall();
// Check count after increment
let count_after = self.count.read();
self
.emit(
ContractUpgraded { old_class_hash, new_class_hash, count_before, count_after },
);
}
Importe call_contract_syscall del módulo syscalls de starknet y get_contract_address de starknet. El contrato completo con las actualizaciones queda así:
use starknet::ClassHash;
#[starknet::interface]
pub trait ICounter<TContractState> {
fn get_count(self: @TContractState) -> u32;
fn increment(ref self: TContractState);
fn upgrade_with_call(ref self: TContractState, new_class_hash: ClassHash);
}
#[starknet::contract]
mod Counter {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
use starknet::syscalls::{call_contract_syscall, replace_class_syscall};
use starknet::{
ClassHash, ContractAddress, SyscallResultTrait, get_caller_address, get_contract_address,
};
#[storage]
struct Storage {
count: u32,
owner: ContractAddress,
current_class_hash: ClassHash,
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
ContractUpgraded: ContractUpgraded,
}
#[derive(Drop, starknet::Event)]
struct ContractUpgraded {
old_class_hash: ClassHash,
new_class_hash: ClassHash,
count_before: u32,
count_after: u32,
}
#[constructor]
fn constructor(ref self: ContractState, owner: ContractAddress, initial_class_hash: ClassHash) {
self.count.write(1); // Initialize counter to 1
self.owner.write(owner); // Sets the contract owner
self.current_class_hash.write(initial_class_hash); // to track the class hashes
}
#[abi(embed_v0)]
impl CounterImpl of super::ICounter<ContractState> {
// returns the current counter value
fn get_count(self: @ContractState) -> u32 {
self.count.read()
}
// increments the counter by 1
fn increment(ref self: ContractState) {
let current = self.count.read();
self.count.write(current + 1);
}
fn upgrade_with_call(ref self: ContractState, new_class_hash: ClassHash) {
let caller = get_caller_address();
assert(caller == self.owner.read(), 'Only owner can upgrade');
// Check count before upgrade
let count_before = self.count.read();
let old_class_hash = self.current_class_hash.read();
replace_class_syscall(new_class_hash).unwrap_syscall();
self.current_class_hash.write(new_class_hash);
// Test: Call increment using call_contract_syscall to see if new implementation is used
let increment_selector = selector!("increment");
call_contract_syscall(get_contract_address(), increment_selector, array![].span())
.unwrap_syscall();
// Check count after increment
let count_after = self.count.read();
self
.emit(
ContractUpgraded { old_class_hash, new_class_hash, count_before, count_after },
);
}
}
}
Vuelva a declarar y desplegar el contrato Counter recientemente actualizado, luego intente actualizar al Greeter usando el class hash original de Greeter. La transacción debería fallar:

Con el error:
Transaction execution has failed: 0: Error in the called contract (contract addre
ss: 0x014154fb6dd088b5ceb46df635ecce6e1a9b0455357931ac7df4263a7dbf39a9, class has
h: 0x036078334509b514626504edc9fb252328d1a240e4e948bef8d0c08dff45927f, selector:
0x015d40a3d6ca2ac30f4031e42be28da9b056fef9bb7357ac5e85627ee876e5ad): Execution fa
iled. Failure reason:(0x617267656e742f6d756c746963616c6c2d6661696c6564 ('argent/m
ulticall-failed'), 0x0 (''), 0x454e545259504f494e545f4e4f545f464f554e44 ('ENTRYPO
INT_NOT_FOUND'), 0x454e545259504f494e545f4641494c4544 ('ENTRYPOINT_FAILED'), 0x45
4e545259504f494e545f4641494c4544 ('ENTRYPOINT_FAILED')).
Esto sucede porque el contrato Greeter no tiene una función increment, por lo que call_contract_syscall no puede encontrar el punto de entrada (entry point) después de la actualización. Esto significa que el contrato de destino debe implementar cualquier función que call_contract_syscall invoque después de la actualización.
Para resolver esto, añadimos una función increment vacía al contrato Greeter:
#[starknet::interface]
pub trait IGreeter<TContractState> {
fn set_greeting(ref self: TContractState, message: ByteArray);
fn get_greeting(self: @TContractState) -> ByteArray;
fn get_greeting_count(self: @TContractState) -> u32;
fn upgrade(ref self: TContractState, new_class_hash: ClassHash);
fn get_owner(self: @TContractState) -> ContractAddress;
//NEWLY ADDED
fn increment(ref self: TContractState); // Added for testing
}
Y la implementamos como una función vacía:
fn increment(ref self: ContractState) {
// This function does nothing - just for demonstration
// In a real scenario, this might have different logic than the Counter's increment
}
Así que tenemos el contrato Greeter actualizado:
use starknet::{ClassHash, ContractAddress};
#[starknet::interface]
pub trait IGreeter<TContractState> {
fn set_greeting(ref self: TContractState, message: ByteArray);
fn get_greeting(self: @TContractState) -> ByteArray;
fn get_greeting_count(self: @TContractState) -> u32;
fn upgrade(ref self: TContractState, new_class_hash: ClassHash);
fn get_owner(self: @TContractState) -> ContractAddress;
// Empty increment function for testing call_contract_syscall
fn increment(self: @TContractState); // NEWLY ADDED
}
#[starknet::contract]
mod Greeter {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
use starknet::syscalls::replace_class_syscall;
use starknet::{ClassHash, ContractAddress, SyscallResultTrait, get_caller_address};
#[storage]
struct Storage {
greeting: ByteArray,
greeting_count: u32,
owner: ContractAddress,
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
ContractUpgraded: ContractUpgraded,
}
#[derive(Drop, starknet::Event)]
struct ContractUpgraded {
new_class_hash: ClassHash,
}
#[constructor]
fn constructor(ref self: ContractState, owner: ContractAddress) {
self.owner.write(owner);
}
#[abi(embed_v0)]
impl GreeterImpl of super::IGreeter<ContractState> {
fn set_greeting(ref self: ContractState, message: ByteArray) {
self.greeting.write(message);
let current_count = self.greeting_count.read();
let new_count = current_count + 1;
self.greeting_count.write(new_count);
}
fn get_greeting(self: @ContractState) -> ByteArray {
self.greeting.read()
}
fn get_greeting_count(self: @ContractState) -> u32 {
self.greeting_count.read()
}
fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {
let caller = get_caller_address();
assert(caller == self.owner.read(), 'Only owner can upgrade');
replace_class_syscall(new_class_hash).unwrap_syscall();
self.emit(ContractUpgraded { new_class_hash });
}
fn get_owner(self: @ContractState) -> ContractAddress {
self.owner.read()
}
// NEWLY ADDED
fn increment(self: @ContractState) { // This function does nothing - just for demonstration
// In a real scenario, this might have different logic than the Counter's increment
}
}
}
Vuelva a declarar el contrato Greeter actualizado para obtener su nuevo class hash, luego llame a upgrade_with_call con ese class hash a través de la pestaña Write Contract de Voyager:

El ContractUpgraded event muestra:

El conteo permaneció sin cambios (1 antes y después) porque la función vacía increment de Greeter no modifica el almacenamiento. Esto demuestra que:
call_contract_syscallejecutó la nueva implementación: Si hubiera usado la antigua lógica deCounter, el conteo habría aumentado a 2.- La actualización fue efectiva de inmediato para el syscall: Se ejecutó la función vacía de
Greeter, no la lógica de incremento delCounter.
En resumen, cualquier llamada directa a funciones dentro de la función de actualización siempre se ejecuta bajo la implementación antigua, independientemente de dónde aparezcan en relación con replace_class_syscall. call_contract_syscall es la única forma de ejecutar la nueva implementación dentro de la misma transacción.
Consideración sobre la Compatibilidad de Almacenamiento
Dado que ambos contratos tienen variables con los mismos nombres (como owner), leen y escriben en las mismas direcciones de almacenamiento calculadas a partir de esos nombres de variables. Cualquier escritura desde la nueva implementación afectará las mismas direcciones de almacenamiento, lo que podría causar pérdida o corrupción de datos si no se maneja con cuidado.
Componentes de Actualización de OpenZeppelin
Los contratos de OpenZeppelin para Cairo proporcionan componentes de actualización estandarizados que manejan patrones de actualización comunes. Su implementación incluye tanto funcionalidad de actualización directa como un patrón de “actualizar y llamar” (upgrade-and-call) que combina replace_class_syscall con call_contract_syscall. El “upgrade-and-call” le permite actualizar e inmediatamente ejecutar funciones de la nueva implementación dentro de la misma transacción, de manera similar a lo que demostramos anteriormente con el uso manual de call_contract_syscall.
Conclusión
El enfoque de Starknet para las actualizaciones de contratos es fundamentalmente diferente a los patrones proxy de Ethereum. Con replace_class_syscall, obtenemos un reemplazo directo de código mientras mantenemos la misma dirección y conservamos todos los datos de almacenamiento.
Recuerde que las actualizaciones conservan todos los datos de almacenamiento; el mismo contrato ahora utiliza el código de la nueva implementación para acceder al almacenamiento, por lo que las variables con nombres coincidentes entre la antigua y la nueva implementación acceden a las mismas direcciones de almacenamiento. El momento exacto también importa: las actualizaciones entran en vigor después de que se completa la función actual, aunque call_contract_syscall puede acceder inmediatamente a la implementación actualizada.