升级

在不同的区块链中,已经开发出多种用于使合约可升级的模式,包括广泛采用的代理模式。

Starknet 通过一个 syscall 原生支持可升级性, 该 syscall 更新合约源代码,从而消除了 对代理的需求

在升级之前,请确保遵循 我们的安全建议

替换合约类

为了更好地理解升级在 Starknet 中是如何工作的,重要的是要理解合约及其合约类之间的区别。

合约类 代表程序的源代码。所有合约都与一个类相关联,并且许多合约可以是同一个类的实例。类通常由一个 类哈希 表示,并且在部署一个类的合约之前,需要声明类哈希。

replace_class_syscall

replace_class syscall 允许合约通过在部署后替换其类哈希来更新其源代码。

/// 将合约源代码升级到新的合约类。
fn upgrade(new_class_hash: ClassHash) {
    assert(!new_class_hash.is_zero(), 'Class hash cannot be zero');
    starknet::replace_class_syscall(new_class_hash).unwrap_syscall();
}
如果一个合约在没有这个机制的情况下部署,它的类哈希仍然可以通过 库调用 替换。

Upgradeable 组件

OpenZeppelin Contracts for Cairo 提供 可升级 以添加对合约的升级支持。

用法

升级通常是非常敏感的操作,并且通常需要某种形式的访问控制,以避免未经授权的升级。本例中使用 可拥有 模块。

我们将使用以下模块来实现 API 参考部分中描述的 IUpgradeable 接口。
#[starknet::contract]
mod UpgradeableContract {
    use openzeppelin_access::ownable::OwnableComponent;
    use openzeppelin_upgrades::UpgradeableComponent;
    use openzeppelin_upgrades::interface::IUpgradeable;
    use starknet::ClassHash;
    use starknet::ContractAddress;

    component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
    component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);

    // Ownable Mixin
    #[abi(embed_v0)]
    impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl<ContractState>;
    impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>;

    // Upgradeable
    impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl<ContractState>;

    #[storage]
    struct Storage {
        #[substorage(v0)]
        ownable: OwnableComponent::Storage,
        #[substorage(v0)]
        upgradeable: UpgradeableComponent::Storage
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        #[flat]
        OwnableEvent: OwnableComponent::Event,
        #[flat]
        UpgradeableEvent: UpgradeableComponent::Event
    }

    #[constructor]
    fn constructor(ref self: ContractState, owner: ContractAddress) {
        self.ownable.initializer(owner);
    }

    #[abi(embed_v0)]
    impl UpgradeableImpl of IUpgradeable<ContractState> {
        fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {
            // This function can only be called by the owner
            self.ownable.assert_only_owner();

            // Replace the class hash upgrading the contract
            self.upgradeable.upgrade(new_class_hash);
        }
    }
}

安全

升级可能是一项非常敏感的操作,在执行升级时,安全性应始终是首要考虑因素。请确保在升级之前彻底审查更改及其后果。需要考虑的一些方面是:

  • 可能会影响集成的 API 更改。例如,更改外部函数的参数可能会破坏调用您的合约的现有合约或链下系统。

  • 可能会导致数据丢失的存储更改(例如,更改存储槽名称,使现有存储无法访问)。

  • 冲突(例如,错误地重用来自另一个组件的相同存储槽)也是可能的,但如果遵循最佳实践,则不太可能发生,例如在存储变量前加上组件的名称(例如 ERC20_balances)。

  • 在 OpenZeppelin Contracts 的版本之间升级之前,请务必检查 向后兼容性

Starknet 中的代理

代理支持不同的模式,例如升级和克隆。但是,由于 Starknet 以不同的方式实现了相同的功能,因此不支持实现它们。

在合约升级的情况下,只需更改合约的类哈希即可实现。至于克隆,合约已经类似于它们实现的类的克隆。

在 Starknet 中实现代理模式有一个重要的限制:没有回退机制可用于将每个潜在的函数调用重定向到实现。这意味着无法实现通用代理合约。相反,一个有限的代理合约可以实现将执行转发到另一个合约类的特定函数。 例如,这对于升级某些函数的逻辑仍然很有用。