Sui Move 合约升级与权限定制
让我们来设计一个简单的猜数程序,用户猜测一个数字并传入函数,判断与程序随机而成的数字是否相同,如果相同就给予一定奖励。
这里的奖励可以是链上流通的货币或是其它有价值的虚拟物品,不过作为一篇适合 $\mathit {Sui\ Move}$ 初学者的简单用例,直接牵扯到高昂物品似乎有所不妥,所以这里的奖励就用一个整型的 $\mathit {prize}$ 来表示,将含有奖金的 $\mathit {object}$ 发送给猜中数字的玩家,以此来作为激励手段。
同时,我们还可以用 $\mathit {Events}$ 来记录链上发生的重大事件。在这个例子里,重大事件无疑就是 $\mathit {Winner}$ 了,我们可以用事件来记录胜者玩家的地址,为了让数据有一定的分析价值,还可以记录到这一次猜中数之前的所有玩家的尝试总数。
那么,根据分析,我们可以用三个 $\mathit {struct}$ 来存储这些信息。
struct Count has key {
id: UID,
total: u64,
}
struct Prize has key {
id: UID,
prize: u8,
}
struct GuessEvent has copy, drop {
total_count: u64,
final_winner: address,
}
sui::event::emit(<ObjectEvent>);
<br>而交易产生的事件详情可以在 $\mathit {Sui\ explorer}$ 中的 $\mathit {Events}$ 标签页查看。根据上述分析,我们可以很轻松地编写三个函数。
fun init(ctx: &mut TxContext) {
let count = Count {
id: object::new(ctx),
total: 0,
};
transfer::share_object(count);
}
fun send_prize(count: u64, prize: u8, ctx: &mut TxContext) {
transfer::transfer(Prize {
id: object::new(ctx),
prize,
}, tx_context::sender(ctx));
event::emit(GuessEvent {
total_count: count,
final_winner: tx_context::sender(ctx),
});
}
public entry fun guess_between_zero_and_hundred(count: &mut Count, number: u8, clock: &Clock, ctx: &mut TxContext) {
let des_number = ((clock::timestamp_ms(clock) % 11) as u8);
if (number == des_number) {
send_prize(count.total, number, ctx);
count.total = 0;
} else {
count.total = count.total + 1;
}
}
好,至此,简单的猜数就设计完毕,但是别忘了这一篇章的主题合约升级与权限定制!
发布的合约 $\mathit {package}$ 是不可变的 $\mathit {object}$,不可撤回也无法修改。智能合约升级的本质是在新的地址上重新发布新的合约,并且把旧版合约的数据迁移过去。
当然,合约升级并不意味着可以对代码进行肆无忌惮地修改,需要满足如下几条规则:
除此之外还需要注意的是,$\mathit {init}$ 函数只会在第一次发布合约时执行,后续合约升级时不会被重复执行。同时,如果你的 $\mathit {package}$ 依赖了一个外部的 $\mathit {package}$,你需要手动把它指向新依赖的合约地址,这一过程在升级合约时不会自动进行。
根据上述内容,我们来思考,想要让一份合约支持后续迭代升级,需要添加一些什么?
assert!(<bool>, <error code>)
,这在前面的篇章当中已有涉及。为了适应更多变的情况,同时提高代码的可读性,可以将 $\mathit {error\ code}$ 定义成一个常量再作为参数传入。修改并整合后的代码如下:
module guess_number::guess_number {
use sui::object::{Self, ID, UID};
use sui::tx_context::{Self, TxContext};
use sui::transfer;
use sui::clock::{Self, Clock};
use sui::event;
const VERSION: u64 = 1;
const ENOTVERSION: u64 = 0;
const ENOTADMIN: u64 = 1;
const ENOTUPGRADE: u64 = 2;
struct Count has key {
id: UID,
version: u64,
admin: ID,
total: u64,
}
struct AdminCap has key {
id: UID,
}
struct Prize has key {
id: UID,
prize: u8,
}
struct GuessEvent has copy, drop {
total_count: u64,
final_winner: address,
}
fun init(ctx: &mut TxContext) {
let admin_cap = AdminCap {id: object::new(ctx)};
let count = Count {
id: object::new(ctx),
version: VERSION,
admin: object::id(&admin_cap),
total: 0,
};
transfer::transfer(admin_cap, tx_context::sender(ctx));
transfer::share_object(count);
}
fun send_prize(count: u64, prize: u8, ctx: &mut TxContext) {
transfer::transfer(Prize {
id: object::new(ctx),
prize,
}, tx_context::sender(ctx));
event::emit(GuessEvent {
total_count: count,
final_winner: tx_context::sender(ctx),
});
}
public entry fun guess_between_zero_and_hundred(count: &mut Count, number: u8, clock: &Clock, ctx: &mut TxContext) {
assert!(count.version == VERSION, ENOTVERSION);
let des_number = ((clock::timestamp_ms(clock) % 11) as u8);
if (number == des_number) {
send_prize(count.total, number, ctx);
count.total = 0;
} else {
count.total = count.total + 1;
}
}
entry fun migrate(count: &mut Count, admin_cap: &AdminCap) {
assert!(count.admin == object::id(admin_cap), ENOTADMIN);
assert!(count.version != VERSION, ENOTUPGRADE);
count.version = VERSION;
}
}
别着急,配置文件 $\mathit {Move.toml}$ 也需要做更改,主要是在 [$\mathit {package}$] 下添加版本号。(这里的 $\mathit {version}$ 和代码中的可以不一致)
[package]
name = "guess_number"
version = "0.0.0"
[addresses]
guess_number = "0x0"
sui move build
sui client publish --gas-budget 100000000
发布成功后在信息里找到跟 $\mathit {package}$ 相关的:<br>
所发布的包的 $\mathit {PackageID}$,以及 $\mathit {AdminCap},\ \mathit {Count},\ \mathit {UpgradeCap}$ 的 $\mathit {PackageID}$ 都是后续会用到的,可以用 $\mathit {export}$ 来赋予一个别名,方便取用。
export PACKAGE_ID=0x628f33fcf96ebc82f275bddb7ad927b0e260e989f2131e0e1dc844ab931b57f5
export ADMIN_CAP=0x2160bdabbe0321d418cd6572ab24092554c3fbea0b5f24fd32295eaf2b8fa63a
export COUNT=0x415f0255873919cc68c217a715454b54537ad3c0677b7d3b925c777131dbf19c
export UPGRADE_CAP=0x57ae075216a7996c944af979a7fe8057f6d1b2165055cdddce9f7898579fc8cd
接下来,我们就可以用下述命令来猜数了:<br>sui client call --package $PACKAGE_ID --module guess_number --function guess_between_zero_and_hundred --args $COUNT 6 0x6 --gas-budget 100000000
<br>其中0x6
作为 $\mathit {Clock}$ 参数的地址传递。
通过sui client object $COUNT
得到的信息可以发现,字段 $\mathit {total}$ 的值为 $\text 1$,说明一共尝试了一次没有成功,此时,重复猜数,直到猜中。
尝试了好几次(永远的$6$),终于在猜数后得到的信息里,$\mathit {Object\ Changes}$ 看到了它新建了一个 $\mathit {Prize}$ 对象,它的拥有者是玩家本人。分别查看 $\mathit {Count}$ 和 $\mathit {Prize}$ 当中存储的内容,如下图所示,发现符合预期。<br>
别忘了事件,除了本文最开始提到的查看方式,其实在执行猜中数的那一次给出的信息当中就有相应的信息:<br>
在上述代价的基础上,首先来思考,除了功能上的升级之外,需要修改什么?
published-at = "<ORIGINAL-PACKAGE-ID>"
,用来标注旧合约地址是哪儿。如果不知道这个信息,又何来数据迁移和升级。// guess_number.move
const VERSION: u64 = 2;
// Move.toml
[package]
name = "guess_number"
version = "0.0.1"
published-at = "0x628f33fcf96ebc82f275bddb7ad927b0e260e989f2131e0e1dc844ab931b57f5" //这里需要替换成你们自己的旧合约地址
接下来,我们来思考哪些功能存在优化的空间?
真实情况下,奖励的东西应该更加吸引人,这样才能诱使人与之进行交易(猜数),所以我们扩大 $\mathit {prize}$,让它等于猜中的那个数乘上一共所猜的次数的十倍,为了不超过 $\mathit u \text 8$ 的数据范围,还需要跟 $\text {255}$ 取个最小值。
本次示例就以这一点为代表进行,只要是符合之前所列举的修改规则的都是可以的,你可以自由发挥(๑•̀ㅂ•́)و✧
经过优化后的代码如下:
module guess_number::guess_number {
use sui::object::{Self, ID, UID};
use sui::tx_context::{Self, TxContext};
use sui::transfer;
use sui::clock::{Self, Clock};
use sui::event;
use sui::math;
const VERSION: u64 = 2;
const ENOTVERSION: u64 = 0;
const ENOTADMIN: u64 = 1;
const ENOTUPGRADE: u64 = 2;
struct Count has key {
id: UID,
version: u64,
admin: ID,
total: u64,
}
struct AdminCap has key {
id: UID,
}
struct Prize has key {
id: UID,
prize: u8,
}
struct GuessEvent has copy, drop {
total_count: u64,
final_winner: address,
}
fun init(ctx: &mut TxContext) {
let admin_cap = AdminCap {id: object::new(ctx)};
let count = Count {
id: object::new(ctx),
version: VERSION,
admin: object::id(&admin_cap),
total: 0,
};
transfer::transfer(admin_cap, tx_context::sender(ctx));
transfer::share_object(count);
}
fun send_prize(count: u64, prize: u8, ctx: &mut TxContext) {
transfer::transfer(Prize {
id: object::new(ctx),
prize,
}, tx_context::sender(ctx));
event::emit(GuessEvent {
total_count: count,
final_winner: tx_context::sender(ctx),
});
}
public entry fun guess_between_zero_and_hundred(count: &mut Count, number: u8, clock: &Clock, ctx: &mut TxContext) {
assert!(count.version == VERSION, ENOTVERSION);
let des_number = ((clock::timestamp_ms(clock) % 11) as u8);
if (number == des_number) {
let prize = (math::min((number as u64) * (count.total + 1) * 10, 255) as u8);
send_prize(count.total, prize, ctx);
count.total = 0;
} else {
count.total = count.total + 1;
}
}
entry fun migrate(count: &mut Count, admin_cap: &AdminCap) {
assert!(count.admin == object::id(admin_cap), ENOTADMIN);
assert!(count.version != VERSION, ENOTUPGRADE);
count.version = VERSION;
}
}
用sui client upgrade --gas-budget 100000000 --upgrade-capability $UPGRADE_CAP
尝试升级合约。
如果出现了如下错误,请考虑修改是否完全遵循了那七条规则。
Error executing transaction: Failure {
error: "PackageUpgradeError { upgrade_error: IncompatibleUpgrade } in command 1",
}
如果升级成功,我们也将得到一长串信息,同样的,将新的 $\mathit {PackageID}$ 记录一下:<br>export NEW_PACKAGE_ID=0x5a3974941fc000890f312c538c47f98c787a89776d6da1762129a4b0379e855b
在调用函数 $\mathit {migrate}$ 之前,旧合约可以正常使用,但是新合约不行,因为 $\mathit {Count}$ 中的 $\mathit {version}$ 还是 $\text 1$,如果此时调用新合约,会出现如下报错:
Error executing transaction: Failure {
error: "MoveAbort(MoveLocation { module: ModuleId { address: 628f33fcf96ebc82f275bddb7ad927b0e260e989f2131e0e1dc844ab931b57f5, name: Identifier(\"guess_number\") }, function: 2, instruction: 14, function_name: Some(\"guess_between_zero_and_hundred\") }, 0) in command 0",
}
如果想要调用 $\mathit {migrate}$ 的话,就需要之前存储的 $\mathit {ADMIN_CAP}$ 的地址,命令如下:<br>sui client call --package $NEW_PACKAGE_ID --module guess_number --function migrate --args $COUNT $ADMIN_CAP --gas-budget 100000000
<br>如果重复调用则会报错,因为 $\mathit {assert}$ 不通过,从错误代码 $\text 2$ 也可以看出,是assert!(count.version != VERSION, ENOTUPGRADE);
出了问题,版本已经不需要更新了。
现在,调用旧合约的话就会出错,新合约就没有问题。不断猜数,观察次数和猜中的数之间的关系,可以发现 $\mathit {prize}$ 的奖励情况变得更诱人了。
如果只是按照之前所叙述的七条修改规则,只要用户想(不考虑代码优美)几乎可以重写所有功能,有的时候这并不是开发者想要的,所以,$\mathit {Sui\ Move}$ 也提供了不同的合约升级权限。
我们尝试执行以下命令,调用0x2
地址(也就是 $\mathit {sui}$ )下的 $\mathit {package}$ 模块中的 $\mathit {make_immutable}$ 函数,将合约升级时必须的 $\mathit {UPGRADE_CAP}$ 传入:<br>sui client call --package 0x2 --module package --function make_immutable --args $UPGRADE_CAP --gas-budget 100000000
此时,再尝试迭代升级,会报错:
Could not find upgrade capability at <address>
这是因为这个函数将 $\mathit {UPGRADE_CAP}$ 销毁了,升级合约的必需品无了,自然也就无法再更新了。
类似的,如果你想要切换成其它权限,也可以用类似的命令,具体的函数名以及参数请移步至此 $\mathit {package.move}$
注意:上述四个权限定制是依次收紧且单向的,也就是说,你可以从 $\mathit {Compatible}$ 收紧成 $\mathit {Immutable}$,但就再也无法松开了,哪怕是 $\mathit {Dependency}$-$\mathit {only}$ 也不行。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!