Stellar Upgrades
Contents
Soroban Upgrade Model
Soroban contracts are mutable by default. Mutability refers to the ability of a smart contract to modify its own WASM bytecode, altering its function interface, execution logic, or metadata. Soroban provides a built-in, protocol-level mechanism for contract upgrades — no proxy pattern is needed.
A contract can upgrade itself if it is explicitly designed to do so. Conversely, a contract becomes immutable simply by not provisioning any upgrade function. This is fundamentally different from EVM proxy patterns:
| Soroban | EVM (proxy pattern) | Starknet |
|---|
| Mechanism | Native WASM bytecode replacement | Proxy delegatecalls to implementation contract | INLINECODE1 swaps class hash in-place |
| Proxy contract needed |
No — the contract upgrades itself | Yes — a proxy sits in front of the implementation | No — the contract upgrades itself |
|
Storage location | Belongs to the contract directly | Lives in the proxy, accessed via delegatecall | Belongs to the contract directly |
|
Opt-in to immutability | Don't expose an upgrade function | Don't deploy a proxy | Don't call the syscall |
One advantage of protocol-level upgradeability is a significantly reduced risk surface compared to platforms that require proxy contracts and delegatecall forwarding.
The new implementation only becomes effective after the current invocation completes. This means if migration logic is defined in the new implementation, it cannot execute within the same call as the upgrade. An auxiliary Upgrader contract can wrap both calls to achieve atomicity (see below).
Using the OpenZeppelin Upgradeable Module
OpenZeppelin Stellar Soroban Contracts provides an upgradeable module in the contract-utils package with two main components:
| Component | Use when |
|---|
Upgradeable | Only the WASM binary needs to be updated — no storage migration required |
UpgradeableMigratable |
The WASM binary and specific storage entries need to be modified during the upgrade |
The recommended way to use these is through derive macros: #[derive(Upgradeable)] and #[derive(UpgradeableMigratable)]. These macros handle the implementation of necessary functions and set the crate version from Cargo.toml as the binary version in WASM metadata, aligning with SEP-49 guidelines.
Upgrade only
Derive Upgradeable on the contract struct, then implement UpgradeableInternal with a single required method:
- -
_require_auth(e: &Env, operator: &Address) — verify the operator is authorized to perform the upgrade (e.g., check against a stored owner address)
The operator parameter is the invoker of the upgrade function and can be used for role-based access control.
Upgrade and migrate
Derive UpgradeableMigratable on the contract struct, then implement UpgradeableMigratableInternal with:
- - An associated
MigrationData type defining the data passed to the migration function - INLINECODE17 — same authorization check as above
- INLINECODE18 — perform storage modifications using the provided migration data
The derive macro ensures that migration can only be invoked after a successful upgrade, preventing state inconsistencies and storage corruption.
Atomic upgrade and migration
Because the new implementation only takes effect after the current invocation completes, migration logic in the new contract cannot run in the same call as the upgrade. An auxiliary Upgrader contract wraps both calls atomically:
CODEBLOCK0
CRITICAL — Upgrader access control: The Upgrader contract MUST have its own access control (e.g., #[only_owner] from the access package). The operator.require_auth() call only proves the operator signed the transaction — it does not prove they are authorized to upgrade the target contract. If the target contract's _require_auth trusts the Upgrader's address (rather than the original caller), then without access control on the Upgrader itself, anyone can trigger upgrades through it.
If a rollback is required, the contract can be upgraded to a newer version where rollback-specific logic is defined and performed as a migration.
Examples: See the examples/ directory of the stellar-contracts repository for full working integration examples of both Upgradeable and UpgradeableMigratable, including the Upgrader pattern.
Access Control
The upgradeable module deliberately does not embed access control itself. You must define authorization in the _require_auth method of UpgradeableInternal or UpgradeableMigratableInternal. Forgetting this allows anyone to replace your contract's code.
Common access control options:
- - Ownable — single owner, simplest pattern (available in the
access package) - AccessControl / RBAC — role-based, finer granularity (available in the
access package) - Multisig or governance — for production contracts managing significant value
Upgrade Safety
Caveats
The framework structures the upgrade flow but does not perform deeper checks:
- - The new contract's constructor will not be invoked — any initialization must happen via migration or a separate call
- There is no automatic check that the new contract includes an upgrade mechanism — an upgrade to a contract without one permanently loses upgradeability
- Storage consistency is not verified — the new contract may inadvertently introduce storage mismatches
Storage compatibility
When replacing the WASM binary, existing storage is reinterpreted by the new code. Incompatible changes corrupt state:
- - Do not remove or rename existing storage keys
- Do not change the type of values stored under existing keys
- Adding new storage keys is safe
- Soroban storage uses explicit string keys (e.g.,
symbol_short!("OWNER")), so key naming is critical — unlike EVM sequential slots, there is no ordering dependency
Version tracking
The derive macros automatically extract the crate version from Cargo.toml and embed it as the binary version in the WASM metadata, following SEP-49. This enables on-chain version tracking and can be used to coordinate upgrade paths.
Testing upgrade paths
Before upgrading a production contract:
- - [ ] Deploy V1 on a local Soroban testnet (e.g.,
stellar-cli with local network) - [ ] Write state with V1, upgrade to V2, and verify that all existing state reads correctly
- [ ] Verify new functionality works as expected after the upgrade
- [ ] Confirm access control — only authorized callers can invoke INLINECODE37
- [ ] Check that V2 includes an upgrade mechanism — otherwise upgradeability is permanently lost
- [ ] Verify storage key compatibility — ensure no removals, renames, or type changes to existing keys
- [ ] Test atomic upgrade-and-migrate using the
Upgrader pattern if migration is needed - [ ] Manual review — there is no automated storage compatibility validation for Soroban; use the derive macros for safe upgrade scaffolding and rely on testnet testing
Stellar 升级
目录
Soroban 升级模型
Soroban 合约默认是可变的。可变性指的是智能合约修改自身 WASM 字节码、改变其函数接口、执行逻辑或元数据的能力。Soroban 提供了一种内置的、协议级别的机制用于合约升级——无需代理模式。
如果合约被明确设计为可升级,它就可以自行升级。相反,只要不提供任何升级函数,合约就变得不可变。这与 EVM 代理模式有根本不同:
| Soroban | EVM(代理模式) | Starknet |
|---|
| 机制 | 原生 WASM 字节码替换 | 代理通过 delegatecall 调用实现合约 | replaceclasssyscall 原地替换类哈希 |
| 是否需要代理合约 |
否——合约自行升级 | 是——代理位于实现合约之前 | 否——合约自行升级 |
|
存储位置 | 直接属于合约 | 位于代理中,通过 delegatecall 访问 | 直接属于合约 |
|
选择不可变 | 不暴露升级函数 | 不部署代理 | 不调用系统调用 |
协议级别可升级性的一个优势是,与需要代理合约和 delegatecall 转发的平台相比,风险面显著降低。
新的实现仅在当前调用完成后生效。这意味着如果新实现中定义了迁移逻辑,它无法在与升级相同的调用中执行。辅助性的 Upgrader 合约可以包装这两个调用以实现原子性(见下文)。
使用 OpenZeppelin 可升级模块
OpenZeppelin Stellar Soroban 合约在 contract-utils 包中提供了一个 upgradeable 模块,包含两个主要组件:
| 组件 | 使用场景 |
|---|
| Upgradeable | 仅需更新 WASM 二进制文件——无需存储迁移 |
| UpgradeableMigratable |
升级期间需要修改 WASM 二进制文件和特定存储条目 |
推荐的使用方式是通过派生宏:#[derive(Upgradeable)] 和 #[derive(UpgradeableMigratable)]。这些宏处理必要函数的实现,并将 Cargo.toml 中的 crate 版本设置为 WASM 元数据中的二进制版本,符合 SEP-49 指南。
仅升级
在合约结构体上派生 Upgradeable,然后实现 UpgradeableInternal,其中包含一个必需方法:
- - requireauth(e: &Env, operator: &Address)——验证操作者是否有权执行升级(例如,对照存储的所有者地址进行检查)
operator 参数是升级函数的调用者,可用于基于角色的访问控制。
升级并迁移
在合约结构体上派生 UpgradeableMigratable,然后实现 UpgradeableMigratableInternal,包含:
- - 一个关联的 MigrationData 类型,定义传递给迁移函数的数据
- requireauth(e, operator)——与上述相同的授权检查
- _migrate(e: &Env, data: &Self::MigrationData)——使用提供的迁移数据执行存储修改
派生宏确保迁移只能在成功升级后调用,防止状态不一致和存储损坏。
原子升级和迁移
由于新实现仅在当前调用完成后生效,新合约中的迁移逻辑无法在与升级相同的调用中运行。辅助性的 Upgrader 合约以原子方式包装这两个调用:
rust
use sorobansdk::{contract, contractimpl, symbolshort, Address, BytesN, Env, Val};
use stellarcontractutils::upgradeable::UpgradeableClient;
use stellarcontractutils::access::Ownable;
#[contract]
pub struct Upgrader;
#[contractimpl]
impl Upgrader {
#[only_owner]
pub fn upgradeandmigrate(
env: Env,
contract_address: Address,
operator: Address,
wasm_hash: BytesN<32>,
migrationdata: sorobansdk::Vec,
) {
operator.require_auth();
let contractclient = UpgradeableClient::new(&env, &contractaddress);
contractclient.upgrade(&wasmhash, &operator);
env.invoke_contract::<()>(
&contract_address,
&symbol_short!(migrate),
migration_data,
);
}
}
关键——Upgrader 访问控制: Upgrader 合约必须有自己的访问控制(例如,来自 access 包的 #[onlyowner])。operator.requireauth() 调用仅证明操作者签署了交易——它不证明操作者有权升级目标合约。如果目标合约的 requireauth 信任 Upgrader 的地址(而不是原始调用者),那么如果没有对 Upgrader 本身的访问控制,任何人都可以通过它触发升级。
如果需要回滚,合约可以升级到更新版本,在该版本中定义回滚特定逻辑并作为迁移执行。
示例: 参见 stellar-contracts 仓库 的 examples/ 目录,获取 Upgradeable 和 UpgradeableMigratable 的完整工作集成示例,包括 Upgrader 模式。
访问控制
upgradeable 模块特意不嵌入访问控制本身。你必须在 UpgradeableInternal 或 UpgradeableMigratableInternal 的 requireauth 方法中定义授权。忘记这一点将允许任何人替换你的合约代码。
常见的访问控制选项:
- - Ownable——单一所有者,最简单的模式(在 access 包中可用)
- AccessControl / RBAC——基于角色,更细粒度(在 access 包中可用)
- 多签或治理——用于管理重要价值的生产合约
升级安全
注意事项
该框架构建了升级流程,但不执行更深层次的检查:
- - 新合约的构造函数将不会被调用——任何初始化必须通过迁移或单独调用进行
- 没有自动检查新合约是否包含升级机制——升级到没有升级机制的合约将永久失去可升级性
- 存储一致性未经验证——新合约可能无意中引入存储不匹配
存储兼容性
替换 WASM 二进制文件时,现有存储将由新代码重新解释。不兼容的更改会破坏状态:
- - 不要删除或重命名现有存储键
- 不要更改现有键下存储的值的类型
- 添加新的存储键是安全的
- Soroban 存储使用显式字符串键(例如 symbol_short!(OWNER)),因此键命名至关重要——与 EVM 顺序插槽不同,这里没有顺序依赖关系
版本追踪
派生宏自动从 Cargo.toml 中提取 crate 版本,并将其作为二进制版本嵌入 WASM 元数据中,遵循 SEP-49。这实现了链上版本追踪,可用于协调升级路径。
测试升级路径
在升级生产合约之前:
- - [ ] 在本地 Soroban 测试网上部署 V1(例如,使用本地网络的 stellar-cli)
- [ ] 使用 V1 写入状态,升级到 V2,并验证所有现有状态读取正确
- [ ] 验证新功能在升级后按预期工作
- [ ] 确认访问控制——只有授权的调用者才能调用 upgrade
- [ ] 检查 V2 是否包含升级机制——否则可升级性将永久丢失
- [ ] 验证存储键兼容性——确保没有删除、重命名或更改现有键的类型
- [ ] 使用 Upgrader 模式测试原子升级和迁移(如果需要迁移)
- [ ] 手动审查——Soroban 没有自动存储兼容性验证;使用派生宏进行安全的升级脚手架,并依赖测试网测试