需求

让合约支持升级

说明

Cairo合约分为两部分:合约类和合约实例。这种划分遵循面向对象编程语言中使用的类似概念,其中我们区分对象的定义实现

合约类是合约的定义:它指定合约的行为方式。它包含基本信息,如开罗字节码、提示信息、入口点名称以及明确定义其语义的所有内容。

为了识别不同的合约类别,Starknet 为每个类别分配了一个唯一的标识符:类别哈希。合约实例是一个已部署的合约,对应于特定的合约类。将其视为 Java 等语言中的对象实例。

每个类都通过其类哈希来标识,这类似于面向对象编程语言中的类名。合约实例是一个类对应的部署合约。

可以通过调用 replace_class_syscall 函数将已部署的合约升级到新版本。通过使用此函数,您可以更新与已部署合约关联的类哈希,从而有效地升级其实现。但是,这不会修改合约的存储,因此合约中存储的所有数据将保持不变。

与solidity合约可升级不同

solidity合约需要特别注意合约数据的存储槽slot位置,cairo合约根据变量的名称区分

solidity合约需要用到delegatecall实现,cairo合约直接变更class_hash

配置引入OpenZeppelin

Scarb.toml添加oz引用

[dependencies]
openzeppelin = { git = "<https://github.com/OpenZeppelin/cairo-contracts.git>", tag = "v0.8.0-beta.0" }

合约添加可升级代码

components提供了一种乐高组合合约代码的方式,教程https://book.cairo-lang.org/zh-cn/ch99-01-05-00-components.html

使用oz upgradeable_component

引入相关文件

use openzeppelin::upgrades::Upgradeable as upgradeable_component;
use openzeppelin::upgrades::interface::IUpgradeable;
use starknet::ClassHash;
use starknet::ContractAddress;

component!宏加载component、storage、enent定义

component!(path: upgradeable_component, storage: upgradeable, event: UpgradeableEvent);

添加impl

impl UpgradeableInternalImpl = upgradeable_component::InternalImpl<ContractState>;

添加storage

#[storage]
struct Storage {
    balance: felt252, 
    #[substorage(v0)]
    upgradeable: upgradeable_component::Storage
}

添加event

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

添加升级代码

#[external(v0)]
impl UpgradeableImpl of IUpgradeable<ContractState> {
    fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {
        // Replace the class hash upgrading the contract
        self.upgradeable._upgrade(new_class_hash);
    }
}

#[generate_trait]会自动生成trait声明,减少模板代码

#[external(v0)]
#[generate_trait]
impl HelloStarknetExtImpl of IHelloStarknetExtTrait {

    fn version(self: @ContractState) -> u8 {
        0
    }
}

v0完整代码

#[starknet::interface]
trait IHelloStarknet<TContractState> {
    fn increase_balance(ref self: TContractState, amount: felt252);
    fn get_balance(self: @TContractState) -> felt252;
}

#[starknet::contract]
mod HelloStarknet {
    use openzeppelin::upgrades::Upgradeable as upgradeable_component;
    use openzeppelin::upgrades::interface::IUpgradeable;
    use starknet::ClassHash;
    use starknet::ContractAddress;

    component!(path: upgradeable_component, storage: upgradeable, event: UpgradeableEvent);

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

    #[storage]
    struct Storage {
        balance: felt252, 
        #[substorage(v0)]
        upgradeable: upgradeable_component::Storage
    }

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

    #[external(v0)]
    impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
        fn increase_balance(ref self: ContractState, amount: felt252) {
            assert(amount != 0, 'Amount cannot be 0');
            self.balance.write(self.balance.read() + amount);
        }

        fn get_balance(self: @ContractState) -> felt252 {
            self.balance.read()
        }
    }

    #[external(v0)]
    impl UpgradeableImpl of IUpgradeable<ContractState> {
        fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {
            // Replace the class hash upgrading the contract
            self.upgradeable._upgrade(new_class_hash);
        }
    }

    #[external(v0)]
    #[generate_trait]
    impl HelloStarknetExtImpl of IHelloStarknetExtTrait {

        fn version(self: @ContractState) -> u8 {
            0
        }
    }
}

v1完整代码

更改version为1

#[starknet::interface]
trait IHelloStarknet<TContractState> {
    fn increase_balance(ref self: TContractState, amount: felt252);
    fn get_balance(self: @TContractState) -> felt252;
}

#[starknet::contract]
mod HelloStarknet {
    use openzeppelin::upgrades::Upgradeable as upgradeable_component;
    use openzeppelin::upgrades::interface::IUpgradeable;
    use starknet::ClassHash;
    use starknet::ContractAddress;

    component!(path: upgradeable_component, storage: upgradeable, event: UpgradeableEvent);

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

    #[storage]
    struct Storage {
        balance: felt252, 
        #[substorage(v0)]
        upgradeable: upgradeable_component::Storage
    }

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

    #[external(v0)]
    impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
        fn increase_balance(ref self: ContractState, amount: felt252) {
            assert(amount != 0, 'Amount cannot be 0');
            self.balance.write(self.balance.read() + amount);
        }

        fn get_balance(self: @ContractState) -> felt252 {
            self.balance.read()
        }
    }

    #[external(v0)]
    impl UpgradeableImpl of IUpgradeable<ContractState> {
        fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {
            // Replace the class hash upgrading the contract
            self.upgradeable._upgrade(new_class_hash);
        }
    }

    #[external(v0)]
    #[generate_trait]
    impl HelloStarknetExtImpl of IHelloStarknetExtTrait {

        fn version(self: @ContractState) -> u8 {
            1
        }
    }
}

实际部署和调用

声明并部署v0版本

将v0版本代码拷贝到lib.cairo中

//declare v0
sncast declare -c HelloStarknet

command: declare
class_hash: 0x371eb3aa14ef0ae390a127824f92acbdb3093cd5b632f49906cb67ae6466886
transaction_hash: 0x29187fcda2c7633a3a64eb86e604fbce445fa9f2da9e9e44769a85e272166c1

//deploy v0
sncast deploy -g 0x371eb3aa14ef0ae390a127824f92acbdb3093cd5b632f49906cb67ae6466886

command: deploy
contract_address: 0x341a70ff2a47079b125a83a72179b5328db92b7217f52bdfba1a41376d30234
transaction_hash: 0x25dacab9a45b22f3bfc57dd2138749d02db4f870cc8dee707b2a80d2406da98

//call version=0
sncast call -a 0x341a70ff2a47079b125a83a72179b5328db92b7217f52bdfba1a41376d30234 -f version    
command: call
response: [0x0]

声明v1版本

将v1版本代码拷贝到lib.cairo中

//declare v1
sncast declare -c HelloStarknet

command: declare
class_hash: 0x768bd8e78e6fbdaad33c9c8d484024822d3d8dd562d6fcd656f8bbebc0ff9be
transaction_hash: 0x53f5c8b7eb2f19ae5019018aad8ce8afe7eed4d621453efff6d5589bf40c048

v0版本升级到v1版本

//invoke update
sncast invoke -a 0x341a70ff2a47079b125a83a72179b5328db92b7217f52bdfba1a41376d30234 -f upgrade -c 0x768bd8e78e6fbdaad33c9c8d484024822d3d8dd562d6fcd656f8bbebc0ff9be 
command: invoke
transaction_hash: 0x3d1f0c7602005ade04d183cd033342d2d73af01e71694680f9da321f0078137

//call version=1
sncast call -a 0x341a70ff2a47079b125a83a72179b5328db92b7217f52bdfba1a41376d30234 -f version                                                                        
command: call
response: [0x1]

也可通过浏览器查询合约class_hash历史版本

Untitled

Contract - Starkscan

d