跳转至

独特的所有权

介绍

在Sui上,每项资产都有它的归属,这个概念与现实世界一致 —— 你拥有一部手机、一台电脑、一辆豪车、一栋别墅...同样的,你也可以拥有一项数字资产,你对该资产全权掌控。但是,一旦资产被转移,之前的所有者就完全失去了对该资产的控制权。

对这个账户独享的所有权的概念进行适当延伸,就可以构造出一种权限控制的方法:在合约中定义一个名为AdminCap的对象,它可以有一个也可以有多个,但是只有被认可的用户才会拥有这个对象,然后在敏感操作的函数中传入该对象进行权限验证...

以下是一个简单的例子:

module admin::admin;

public struct AdminCap has key {
    id: UID
}

fun init(ctx: &mut TxContext) {
    transfer::transfer(AdminCap {
        id: object::new(ctx)
    }, ctx.sender());
}

public fun sensitive_operation(_: &AdminCap) {
    // do something...
}

没错,你只需要把AdminCap传入,不需要做任何显式操作,它会自动对你传入的对象进行类型判断:<package::module::object_name>

例题

接下来,考虑这么一个问题:

module admin::admin;

use sui::event;
use sui::package::{Self, Publisher};

// 一次性见证者结构,结构名是模块名的全大写,只有在`init`中使用
public struct ADMIN has drop {}

public struct Flag has copy, drop {
    user: address,
    success: bool
}

// 仅在发布上链时自动调用一次
fun init(otw: ADMIN, ctx: &mut TxContext) {
    // 使用一次性见证者`ADMIN`,创建`Publisher`并转移其所有权给发布者
    // 每个模块都可以生成模块对应的`Publisher`
    package::claim_and_keep(otw, ctx);
}

// 尝试使用`Publisher`进行权限控制,阻止夺旗
public fun get_flag(_: &Publisher, ctx: &TxContext) {
    event::emit(Flag {
        user: ctx.sender(),
        success: true
    });
}

按照前文所述,在调用函数时会自动对传入的对象进行类型判断,而唯一的Publisher已经转移给了该合约的发布者,那么该如何破局夺旗?

前文也提到,<package::module::object_name>是简单类型判断的依据,那么Publisher从何而来 —— sui::package::Publisher!!!换句话说,每一个Publisher(不同模块甚至是不同合约),都可以通过该例题的校验!!!

所以,对于这道例题而言,想要夺旗,只需要自己发布并得到一个Publisher,用它来get_flag

那么,Publisher真的无法用来做权限控制吗?

sui::package模块中,提供了两个函数:from_packagefrom_module,需要根据实际情况手动调用进行断言。但对权限控制而言,直接使用Publisher局限性非常大,不如自己构造一个AdminCap灵活且更具扩展性。

拓展

在Sui上,除了独享所有权,还有共享所有权和冻结(不可变)所有权(可视作特殊的共享,允许只读访问,无法修改和转移)。但是,所有的操作都只能是其原合约中定义的函数功能,也就是说,即使是独享/共享所有权,你也无法对它为所欲为。

举一反三,是不是又可以发现几个可供审查的漏洞?比如:将敏感的对象误设为共享;本应独享的控制权却存储于共享的对象当中......