数据迁移
智能合约升级还涉及到数据迁移,将旧版合约中的数据移动到新版合约中来。如果没有数据迁移,另外重新发布一个合约也行。
共享的 Object
记录的数据通常会以共享的 object 存放,合约升级之后,新版和旧版的合约其实都还可以对该共享的 object 进行操作。
有的时候会因为函数不兼容或给共享 object 添加了动态属性导致不兼容造成出错。可以考虑给共享 object 添加版本属性,限制只允许新版的合约进行操作,打破共享 object 的往后兼容,强迫用户选择升级。
计数器合约升级
以一个计数器的案例来演示如何进行合约升级。
实现目标
原始合约是使用了共享 object 来累加计数
public entry fun increment(c: &mut Counter) {
    c.value = c.value + 1;
}
升级后的合约添加了每累加 100 就发出 event 信息的功能
struct Progress has copy, drop {
    reached: u64
}
public entry fun increment(c: &mut Counter) {
    c.value = c.value + 1;
    if (c.value % 100 == 0) {
        event::emit(Progress { reached: c.value });
    }
}
只实现了原本计数功能的合约可以在这里找到完整代码。
修改合约以支持升级
对原本计数功能的合约进行修改,以支持后续合约升级中的数据迁移。
- 在合约当前 module 中使用常量 VERSION记录当前的版本信息.
    const VERSION: u64 = 1;
- 在共享 object Counter中添加新的version属性来记录当前共享 object 的版本信息。
    struct Counter has key {
        id: UID,
        // 2. Track the current version of the shared object
        version: u64,
        // 3. Associate the `Counter` with its `AdminCap`
        admin: ID,
        value: u64,
    }
- 让执行共享 object 版本升级数据迁移的操作成为专有操作,并只允许使用 AdminCap来调用。
    /// Not the right admin for this counter
    const ENotAdmin: u64 = 0;
    fun init(ctx: &mut TxContext) {
        let admin = AdminCap {
            id: object::new(ctx),
        };
        transfer::share_object(Counter {
            id: object::new(ctx),
            version: VERSION,
            admin: object::id(&admin),
            value: 0,
        });
        transfer::transfer(admin, tx_context::sender(ctx));
    }
- 确保所有调用了共享 object 的 entry 函数都会检查,确保共享 object 的版本属性 version与合约版本VERSION一致。
    /// Calling functions from the wrong package version
    const EWrongVersion: u64 = 1;
    public entry fun increment(c: &mut Counter) {
        // 4. Guard the entry of all functions that access the shared object with a version check.
        assert!(c.version == VERSION, EWrongVersion);
        c.value = c.value + 1;
    }
修改到可以支持升级的合约可以在这里找到完整代码。
这时候 package 配置文件 Move.toml 的格式是这样的
[package]
name = "sui_package"
version = "0.0.0"
[addresses]
sui_package = "0x0"
使用 Sui CLI 发布合约
sui client publish --gas-budget <GAS-BUDGET-AMOUNT>
并且记录下发布后的地址信息

Immutable 是合约发布地址,Shared 是该合约中共享的 object.
使用 suiexplorer 或者 
sui client object <OBJECT-ID>
判别区分出AdminCap和UpgradeCap. 其中AdminCap在后续共享object数据迁移中用来管理权限,UpgradeCap是合约升级的关键权限,在下一节定制升级权限做更详细讲解。
升级后的合约
升级后的合约除了功能更新之外,还需要做以下额外修改。
- 增大 package 的VERSION.
    // 1. Bump the `VERSION` of the package.
    const VERSION: u64 = 2;
- 引入 migrate函数来升级共享 object.
    // 2. Introduce a migrate function
    entry fun migrate(c: &mut Counter, a: &AdminCap) {
        assert!(c.admin == object::id(a), ENotAdmin);
        assert!(c.version < VERSION, ENotUpgrade);
        c.version = VERSION;
    }
需要注意的是,migrate 函数是非public的entry函数,这让 migrate 函数可以被 Sui CLI 或 SDK 调用,但无法被其他 module 调用。这种权限允许了将来升级的合约可以自由修改输入的参数格式。还做了 AdminCap 权限管理的检查以及确保合约先升级再进行共享object升级的检查。
在合约升级之后,共享的 object 并不会自动升级,还需要调用 migrate 函数升级。
其他功能更新
    struct Progress has copy, drop {
        reached: u64,
    }
    /// Not the right admin for this counter
    const ENotAdmin: u64 = 0;
    /// Migration is not an upgrade
    const ENotUpgrade: u64 = 1;
    /// Calling functions from the wrong package version
    const EWrongVersion: u64 = 2;
    public entry fun increment(c: &mut Counter) {
        assert!(c.version == VERSION, EWrongVersion);
        c.value = c.value + 1;
        if (c.value % 100 == 0) {
            event::emit(Progress { reached: c.value })
        }
    }
升级后的合约可以在这里找到完整代码。
对配置文件 Move.toml 进行编辑,<ORIGINAL-PACKAGE-ID> 填写之前发布合约的地址。
[package]
name = "sui_package"
version = "0.0.1"
published-at = "<ORIGINAL-PACKAGE-ID>"
[addresses]
sui_package = "0x0"
运行合约升级命令行,其中<UPGRADE-CAP-ID>处填写UpgradeCap的Object ID.
sui client upgrade --gas-budget <GAS-BUDGET-AMOUNT> --upgrade-capability <UPGRADE-CAP-ID>
运行结果如下,表明合约升级成功,其中出现的 Immutable 是升级后合约的发布地址。

