在 Ethereum 上,代理模式(proxy pattern)是实现合约可升级性最常见的方法。在这种模式下,代理合约持有合约的存储,并将函数调用委托给一个独立的实现合约。当需要升级时,你需要部署一个新的实现合约,并将代理指向它。
Starknet 采取了不同的方法,它使用 replace_class_syscall 来替换已部署合约的 class hash,而不改变其存储和地址。本文将介绍 replace_class_syscall 的工作原理以及如何在 Starknet 上实现合约升级。
replace_class_syscall 的工作原理
回顾一下在 “Understanding Starknet’s Contract Deployment Model” 这一章中所讲的,每个合约都是一个合约类的实例(instance of a contract class):类包含字节码,而实例持有存储和它的地址。因为字节码和存储是分开存放的,所以你可以在保留存储的同时,将合约实例指向一个新的类。
要使用 replace_class_syscall 升级合约,我们需要将新实现的 class hash 作为参数(new_class_hash: ClassHash)传入。
replace_class_syscall 的函数签名如下:
fn replace_class_syscall(new_class_hash: ClassHash) -> SyscallResult<()>
它返回 SyscallResult<()>:这是一个结果类型,成功时包装 Ok(()),失败时包装带有错误详细信息的 Err。当该 class hash 尚未在 Starknet 上声明,或者调用者没有执行升级的权限时,系统调用(syscall)将会失败。
当 replace_class_syscall 执行成功时:
- 合约地址保持不变(无论进行多少次升级)
- 所有存储数据均保留在合约自身的存储中
- 一旦执行返回给调用者,合约实例将开始使用来自新 class hash 的逻辑。
既然我们已经了解了 replace_class_syscall 的工作原理,接下来让我们用它来升级一个合约。
使用 replace_class_syscall 进行合约升级
我们将把一个能将计数加 1 并获取当前计数的 Counter 合约,升级到一个新的类实现,即 Greeter 合约。
Counter 合约
为了跟随本教程,请创建一个新项目并进入该目录:
scarb new counter_upgrade
cd counter_upgrade
然后创建一个 src/counter.cairo 文件并添加以下代码。
我们将重点关注 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 });
}
}
}
upgrade 函数接受新的 class hash 作为参数,并确保调用者是合约的所有者,以防止未经授权的升级。
let caller = get_caller_address();
assert(caller == self.owner.read(), 'Only contract owner can upgrade');
实际的升级发生在这行代码:
replace_class_syscall(new_class_hash).unwrap_syscall();
如果 replace_class_syscall 返回错误,unwrap_syscall() 会引发 panic,从而导致交易回滚(revert)。这意味着升级要么成功完成,要么交易发生 panic 并回滚,使合约保持原样不变。
一旦升级完成,该函数会触发(emit)一个 ContractUpgraded 事件来记录这一变化:
self.emit(ContractUpgraded { old_class_hash, new_class_hash });
将 src/lib.cairo 的内容替换为 mod counter;,以告诉编译器在构建中包含哪些模块。
声明和部署 Counter 合约
现在让我们声明并部署该合约,以测试升级功能。
运行以下命令来声明 Counter 合约。将 YOUR_ACCOUNT_NAME 替换为你的账户名,将 YOUR_API_KEY 替换为你的 Alchemy API 密钥:
sncast \
--account <YOUR_ACCOUNT_NAME> \
declare \
--url https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/<YOUR_API_KEY> \
--contract-name Counter

部署 Counter 合约
由于 Counter 的构造函数需要初始的 class hash 作为参数来跟踪升级,我们将上面声明步骤中获得的 class hash 与所有者地址一起传递给它。运行以下命令进行部署,将其中的占位符替换为具体的值:
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>

部署后,count 的值应为 1,因为我们在构造函数中将其初始化为 1。我们可以通过 Voyager 的“读取合约(read contract)”界面调用 get_count 来验证这一点,它将返回 1。

要升级 Counter 合约,我们需要一个新的 class hash。我们可以通过声明第二个名为 Greeter 的合约来获取它。
创建 Greeter 合约
Greeter 合约将用于设置和获取问候消息、跟踪问候次数,并包含升级功能。我们这里使用一个结构上不同的合约,而不是一个新版本的 Counter,是为了演示以下三点:
- 存储如何在升级过程中得以保留
- 字段名称冲突时的表现如何
- 在调用
replace_class_syscall之后,新实现确切的生效时机
下面的章节将对这些内容逐一进行讲解。
在同一个 counter_upgrade 项目中创建一个 src/greeter.cairo 文件,并向其中添加以下代码:
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()
}
}
}
然后将 mod greeter; 添加到 src/lib.cairo 中,这样我们的 lib.cairo 内容如下:
mod counter;
mod greeter;
声明 Greeter 合约以获取用于升级 Counter 合约的 class hash:
sncast \
--account <YOUR_ACCOUNT_NAME> \
declare \
--url https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/<YOUR_API_KEY> \
--contract-name Greeter

使用 Counter 部署期间设置为 owner 的地址,在 Voyager 的“写入合约(Write Contract)”标签页中调用 upgrade 函数,将 Greeter 的 class hash 作为参数传入,此时交易应当成功:

升级前后的合约存储
在升级之前,Counter 合约的状态如下:
- count =
1 - owner =
0x014154fb6Dd088b5ceB46df635eCCe6e1a9B0455357931aC7Df4263A7dBf39a9 - current_class_hash =
0xd6574c9c64c779442f0f958db1935708b09d18a4cfb98a86e0ac1ded53ebd9
升级之后,合约保留了其所有存储数据,同时从 Greeter 的 class hash 执行代码:
- count =
1 - owner =
0x014154fb6Dd088b5ceB46df635eCCe6e1a9B0455357931aC7Df4263A7dBf39a9 - current_class_hash =
0x6cc6a96920706f49a5579a2c0f235e8480daa047500e4acc3910b5da0c010c0 - greeting =
0 - greeting_count =
0
请注意,两个合约都有一个 owner 字段,该字段映射到相同的存储位置(由 sn_keccak("owner") 计算得出)。
我们可以使用 Voyager 的存储查询界面来验证这一点。Voyager 当前的界面一次只允许你查询一个存储槽(storage slot)。
如果想一次性查询所有存储槽,请点击合约页面顶部的 “View old version of this page(查看此页面的旧版本)”,并从查询类型的下拉菜单中选择 “Struct”。

然后将以下结构体粘贴到输入框中。请注意,此结构体组合了 Counter 和 Greeter 类实现中的所有存储变量:
#[storage]
struct Storage {
count: u32,
owner: ContractAddress,
current_class_hash: ClassHash,
greeting: ByteArray,
owner: ContractAddress,
}
点击 “Query Struct Data(查询结构体数据)”,可以看到相同的存储如何通过合约结构体被解析:

请注意,新的 class hash 仅在当前 upgrade 函数调用结束之后,才会对后续的调用生效。
参考以下示例代码,它准确展示了升级功能在 upgrade 函数内何时生效:
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
}
- 在
upgrade调用执行期间:count_before = 1,接着使用旧逻辑执行self.increment()后,count_after = 2 - 在升级函数完成之后:对合约的后续调用将执行来自新 class hash 的代码
这意味着 replace_class_syscall 注册了新的 class hash,但当前的调用仍将继续执行旧类的代码。如果你需要在同一笔交易内执行新类的代码,请将 replace_class_syscall 和 call_contract_syscall 结合使用。
结合使用 replace_class_syscall 与 call_contract_syscall 进行升级
在升级时,总是先调用 replace_class_syscall 来注册新类。如果你随后需要在同一个函数中立即调用新的实现,你需要接着使用 call_contract_syscall。
在 replace_class_syscall 之后对同一合约发起的任何 call_contract_syscall 调用都将使用新的实现来执行,即使升级函数本身内部的直接函数调用依然在旧实现下运行。
这里重写了之前的升级函数,使用 call_contract_syscall 替代 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)
}
与 self.increment() 会在整个升级函数期间继续使用旧实现不同,call_contract_syscall 会将增量调用(increment call)派发到新类。由于 replace_class_syscall 已经将合约更新为指向该新类,因此 call_contract_syscall 执行的是新的实现。这就是为什么 count_after 仍然是 1,而不是增加到 2。
让我们通过一个包含这两种独立升级函数的已更新的 Counter 合约,在链上验证这两种升级方法。
版本 1:标准的 replace_class_syscall
我们将通过在同一函数内调用 increment() 之前和之后检查计数,来测试升级是否立即生效。
首先,更新 Counter 中的 ContractUpgraded 事件以包含对计数的跟踪:
#[derive(Drop, starknet::Event)]
struct ContractUpgraded {
old_class_hash: ClassHash,
new_class_hash: ClassHash,
count_before: u32, //ADD THIS
count_after: u32, //ADD THIS
}
将 upgrade 函数替换为 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
});
}
然后,更新接口以反映 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
}
以下是包含 upgrade_standard 函数的完整更新后的 Counter 合约:
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 },
);
}
}
}
重新声明并部署更新后的 Counter 合约,然后通过 Voyager 的 Write Contract 标签页传入 Greeter 的 class hash 调用 upgrade_standard。

触发的(emitted)ContractUpgraded 事件显示计数从 1 增加到了 2,这证实了在升级函数执行期间使用的是旧实现的 increment() 函数:

版本 2:在升级函数内使用 call_contract_syscall
现在让我们创建一个使用 call_contract_syscall 的升级版本,以确认我们是否能在同一笔交易内立即访问到新的实现。
将 upgrade_standard 函数替换为以下使用 call_contract_syscall 的 upgrade_with_call 实现:
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 },
);
}
从 starknet 的 syscalls 模块导入 call_contract_syscall,并从 starknet 导入 get_contract_address。包含这些更新的完整合约如下:
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 },
);
}
}
}
重新声明并部署新更新的 Counter 合约,然后尝试使用最初的 Greeter class hash 升级到 Greeter。此时该交易将会失败:

伴随如下错误:
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')).
发生这种情况是因为 Greeter 合约没有 increment 函数,因此 call_contract_syscall 在升级后找不到入口点(entry point)。这意味着目标合约必须实现 call_contract_syscall 在升级后调用的任何函数。
为了解决这个问题,我们在 Greeter 合约中添加一个空的 increment 函数:
#[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
}
并将其实现为空函数:
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
}
于是我们得到了更新后的 Greeter 合约:
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
}
}
}
重新声明更新后的 Greeter 合约以获取其新的 class hash,然后通过 Voyager 的 Write Contract 标签页使用该 class hash 调用 upgrade_with_call:


计数保持不变(升级前后均为 1),因为 Greeter 的空 increment 函数并没有修改存储。这证明了:
- **
call_contract_syscall执行了新的实现:**如果它使用了旧的Counter逻辑,计数就会增加到 2 - **对于该 syscall,升级是立即生效的:**运行的是空
Greeter函数,而不是Counter的递增逻辑
总结而言,升级函数内部的任何直接函数调用都总是在旧实现下运行的,无论它们出现在相对于 replace_class_syscall 的哪个位置。call_contract_syscall 是在同一笔交易内执行新实现的唯一途径。
存储兼容性考虑
由于两个合约都有同名变量(例如 owner),它们会对由这些变量名计算出的相同存储地址进行读写操作。来自新实现的任何写入都会影响相同的存储地址,如果管理不当,可能会导致数据丢失或损坏。
OpenZeppelin 升级组件
适用于 Cairo 的 OpenZeppelin 合约提供了处理常见升级模式的标准升级组件。它们的实现既包含了直接升级功能,也包含了将 replace_class_syscall 与 call_contract_syscall 相结合的升级并调用(upgrade-and-call)模式。升级并调用模式允许你在同一笔交易中进行升级并立即执行新实现中的函数,这与我们前面手动使用 call_contract_syscall 演示的效果相似。
结论
Starknet 处理合约升级的方法与 Ethereum 的代理模式有着根本的不同。借助 replace_class_syscall,我们可以直接替换代码,同时保留相同的地址和所有存储数据。
请记住,升级会保留所有存储数据;同一个合约现在只是使用新实现的代码来访问存储,因此在旧实现和新实现之间名称匹配的变量将访问相同的存储地址。时机也很重要:升级在当前函数完成之后才会生效,不过 call_contract_syscall 能够立即访问到升级后的实现。