Move 研究 - 第二部分 Sui

  • zellic
  • 发布于 2025-02-12 21:50
  • 阅读 20

本文介绍了 Sui 这一基于 Move 语言的新区块链,强调了与 Aptos 的比较和其在安全智能合约开发上的重要性。Sui 通过独特的对象模型和状态管理方式,提升了数字资产的实时使用场景,并着重分析了 Sui Move 的特性以及潜在的安全问题。

这篇博客文章提供了对 Sui 的一般介绍,这是一个基于 Move 的新区块链。我们将 Sui 与 Aptos 进行比较,并强调一些开发者需要了解的重要区别,特别是在安全智能合约开发方面。

引言

Sui 是一个面向 Move↗ 语言的权证证明区块链平台。Sui 的独特之处在于每个对象都有自己的账本,交易不必按顺序进行。这被称为突破性的优化,对 Sui 区块链上的对象处理具有重要影响。

Sui 区块链上使用的 Move 语言最初是在 Facebook 为 Libra 区块链(后来的 Diem)开发的。该项目已被中止,开发团队分成两组,创建了 Sui↗Aptos↗,这都是运行 Move 的第一层区块链。还有一些其他区块链也在构建在 Move 的基础上。

Sui 旨在支持数字资产的实时使用案例,例如游戏中的物品。交易需要足够快速,以确保合理的游戏体验。

在技术方面,验证者和客户端使用 Rust 编写,治理部分在链上用 Move 编写。此外,还有一个 Sui Move 标准库预先部署在链上,智能合约可以调用。原始 Move 标准库的一个有限版本也可用。

为了帮助解释 Sui,我们将 Sui Move 与 Aptos Move 和核心 Move 进行比较。

Sui 对象模型

账户

区块链(和 Aptos)通常使用账户来管理权限和所有权问题。账户本质上就是地址。账户与私钥相关联,可以用于签署交易。在 Sui 中,资源或称为 _对象_主要是保存在一个账户下。

例如,在 Sui 中持有 Coin 资源将允许你使用该账户的 SUI(货币)来签署交易,以支付运行某个函数的 gas 费用。

关于资源和账户更深入的讨论,以及它如何在 Aptos 中工作,请查看我们关于 Aptos 的博客文章。

对象

核心、Aptos 和 Sui Move 中的对象用于表示资产,例如 USDT、智能合约的管理员权限、链上扑克游戏中的筹码,以及智能合约正在跟踪的任何其他数据。无需赘言,对象及其管理方式是 Move 智能合约的核心,无论是在 Aptos 还是 Sui 上。

对象本质上只是作为 Move 中的 struct 表示的相关数据的集合。Move struct 可以有四种类型的能力:keystorecopydrop

能力

  • key 使对象能够存储在链上。如果一个对象没有 key,它必须存储在另一个对象 之下 或者在合约执行完成前被销毁。
  • store 允许对象存储在另一个对象 之下。可以将 struct 理解为盒子,store 允许这个特定的盒子放入其他盒子中。
  • copy 允许对象被复制。不可复制的对象不能被复制,但理论上可以通过创建合约手动“复制”。
  • drop 允许对象静默销毁。简单地让对象超出作用域将销毁它。

在 Sui 中,为了一个 Move 对象能够在链上存储在一个账户之下,它必须也是一个 Sui 对象。Sui 对象具有 key 能力(因为它们存储在链上)以及一个名为 id 的特殊首个成员,其类型为 UID。字节码验证器确保任何具有 key 能力的 struct 都具有该特殊首个成员。

对象 ID 在 Sui Move 中实现为 UID。在 Sui 中,UID 是唯一的,并用于从链上智能合约和 Sui 客户端的链下引用对象。在 Sui 中,想要将对象传递给 Move 合约,必须通过其 UID 来引用该对象。

UID 旨在对每个链上对象是唯一的。UID 可以在 RPC 调用或 Sui 命令行中使用。UID 还可用于请求有关链上对象状态的信息。

Sui Move 和核心/Aptos Move 之间最显著的区别在于 Sui 不利用 Move 全球存储。

在 Aptos/核心 Move 中,任何具有 key 能力的对象可以存储在一个账户下(不需要 UID)。Aptos 智能合约可以在其执行的任何点访问它所定义的任何账户下的对象类型,即使调用者并不是被访问的账户。这通常通过 borrow_global 来完成,借用对其定义的对象类型的引用。

而在 Sui 的情况下,对象在顶层调用时直接传递给运行时,并且在执行过程中不能借用外部引用。要存储对象,你可以使用 transfer::transfer(object, address) 函数。完整的示例可在本文的 UID 交换部分找到。

在 Sui 中,智能合约可以访问的所有对象都是从 Move 外部传递进来的。这类似于 Solana 每个交易的预声明访问账户。只有账户的所有者可以直接或间接(通过引用)将对象传递到 Move 中。因此,存储在发送者以外的其他账户下的对象是不可访问的(共享和冻结的对象除外)。对象的所有权在决定谁可以做什么方面是至关重要的。USDTCoin 的所有者可以消费它。WithdrawPermission 的所有者可以从闪电贷方提取资金。

Sui 对象也可能在某些特殊状态下,以处理简单账户所有权以外的情况。在 Sui Move 执行的任何时刻,Sui 对象可以处于六种状态之一。

Sui 对象状态

  • 在 Move 合约内部
    • Sui 对象是通过值传递到 Move 合约内,或在 Move 合约内部创建的。必须在 Move 执行结束之前将其转移到某个账户(如果有 key)或销毁。
  • 冻结/不可变(全局只读)
    • Sui 对象可以被任何人通过只读引用传递到 Move 合约。
  • 共享(全局读写)
    • Sui 对象可以被任何人通过读写引用传递到 Move 合约。
  • 由一个地址拥有
    • 只有该地址可以将对象传递给 Move(作为引用或直接)。
    • 在核心 Move 中,声明合约可以从任何地址使用其声明的类型,而在 Sui 中,只有调用者拥有的对象可以被传递到 Move 中。
    • 在核心 Move 中,某种类型的对象只能存储在一个地址下,但 Sui 中没有这样的限制。
  • 由另一个对象拥有
    • 截至写作时(2022 年 11 月),此功能尚未实现,机制不清晰。
    • 根据当前文档,该对象仍将可以直接在链上访问。
  • 包装
    • 对象具有 keystore,并且存储在某个以链上某处存储的另一个对象下(作为成员直接存储或嵌套在多个对象下)。
    • 对象在链上无法直接访问。对象存储在另一个包装对象中。要恢复原始对象,必须销毁包装对象(拆分)。

对象 ID

对象 UID 应该是全局唯一的。它们通过 object::new(&mut TxContext) 创建,并通过 object::delete(UID) 销毁。

其他 Sui 区别

TxContext

在 Aptos 中,交易的 signer 作为传入函数的参数直接接收。 在 Sui 中,TxContext 类型可以像与 signer 的使用一样作为函数的参数声明,并且可以使用库函数从 TxContext 获取 signer。此外,TxContext 用于在 Sui Move 中创建 UID。截至目前,TxContext 仅用于这些目的。

全球存储和 acquires 关键字

Aptos 使用来自核心 Move 的全球存储操作符(move_tomove_from 等)。使用全球存储的函数必须拥有适当的 acquires 注解。

不出所料,Sui 中不使用 acquires 关键字,以及全球存储操作符,因为 Sui 不使用 Move 全球存储。

init(&mut TxContext) 函数

Aptos 支持在模块部署时调用的初始化函数,具有类似的签名。Sui 将在模块部署时调用具有此签名的 init 函数。

有趣的行为和潜在的漏洞

对象所有权转移

Sui 对象(具有 key+store)存储在一个账户中可以在账户之间自由转移,而不需创建合约的参与。这可能会潜在引起直觉上的问题,并导致依赖账户持有某些合约创建对象组合的合约出现问题,只具有 key 能力的对象则受到了更多限制。它们只能通过创建模块转移。

对象冻结

存储在账户中的 Sui 对象如果同时具有 store,可以在没有创建合约参与的情况下被冻结。冻结对象是全局的且不可变。任何人都可以将其传递到智能合约,这可能会令期望对象只能来自单一账户的开发者感到惊讶。

重入

Move↗ 中,不可能出现重入,因为不支持动态回调。这在以太坊/Solidity 合约中是一个经典错误,智能合约调用一个回调函数,该函数又调用回合约,可能操作该智能合约的部分更新状态。到这一点时,函数调用尚未结束;它正在等待回调完成。你可以在这篇 Consensys 文章 中找到更多详细信息。

整数溢出

在 Move 中,整数溢出会自动回滚。任何导致整数溢出的交易都不能成功。然而,位运算符不会溢出,即使最终结果似乎不合常理。例如,超出变量位宽的位移是允许的,被移入的位是零。我们在我们的 Aptos Move 文章中讨论了这个问题。

Sui 对象 UID 交换

你可能知道,UID 应该唯一识别链上的对象。然而,如果你控制着创建这两个对象的合约,因此可以销毁/重新创建你可以在链上更改对象的类型。

struct Cat has key {
    id: UID,
}

struct Dog has key {
    id: UID,
}

public entry fun transmute(cat: Cat, dog: Dog, ctx: &mut TxContext) {
    let Cat {
        id: cat_id,
    } = cat;
    let Dog {
        id: dog_id,
    } = dog;
    let new_cat = Cat {
        id: dog_id,
    };
    let new_dog = Dog {
        id: cat_id,
    };
    transfer::transfer(new_cat, tx_context::sender(ctx));
    transfer::transfer(new_dog, tx_context::sender(ctx));
}

让我们在链上尝试这个合约。

首先我们检查我们的猫和狗的类型:

$ sui client object --id 0x1ae0eb5f3b972449ff2a4134e799f447c662939e

----- Move Object (0x1ae0eb5f3b972449ff2a4134e799f447c662939e[1]) -----
Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )
Version: 1
Storage Rebate: 12
Previous Transaction: DfF7MGmkZcl4YuLT7gnLXsyfmZhNv/d/72V9L0eOBc0=
----- Data -----
type: 0xccfefb2987fabb577cb4e5cb726d7d3a08c7f2e2::objid::Cat
id: 0x1ae0eb5f3b972449ff2a4134e799f447c662939e

$ sui client object --id 0x49dc71ac8bb7b649c8ff21d255295da72d791707

----- Move Object (0x49dc71ac8bb7b649c8ff21d255295da72d791707[1]) -----
Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )
Version: 1
Storage Rebate: 12
Previous Transaction: DfF7MGmkZcl4YuLT7gnLXsyfmZhNv/d/72V9L0eOBc0=
----- Data -----
type: 0xccfefb2987fabb577cb4e5cb726d7d3a08c7f2e2::objid::Dog
id: 0x49dc71ac8bb7b649c8ff21d255295da72d791707

然后我们调用 transmute(cat, dog)

$ sui client call --package 0xccfefb2987fabb577cb4e5cb726d7d3a08c7f2e2 --module objid --gas-budget 10000 --function transmute --args 0x1ae0eb5f3b972449ff2a4134e799f447c662939e 0x49dc71ac8bb7b649c8ff21d255295da72d791707

----- Certificate ----
<snip>
----- Transaction Effects ----
Status : Success
Mutated Objects:
  - ID: 0x1ae0eb5f3b972449ff2a4134e799f447c662939e , Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )
  - ID: 0x37d38af0f4cac30679658c3a75cbbf90169d82d7 , Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )
  - ID: 0x49dc71ac8bb7b649c8ff21d255295da72d791707 , Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )

然后再次检查我们的猫和狗的类型:

$ sui client object --id 0x1ae0eb5f3b972449ff2a4134e799f447c662939e

----- Move Object (0x1ae0eb5f3b972449ff2a4134e799f447c662939e[2]) -----
Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )
Version: 2
Storage Rebate: 12
Previous Transaction: kTbgQmSch/kav+EW2abSfM3w4Seujv3UK7Yn4ZUV34Q=
----- Data -----
type: 0xccfefb2987fabb577cb4e5cb726d7d3a08c7f2e2::objid::Dog
id: 0x1ae0eb5f3b972449ff2a4134e799f447c662939e

$ sui client object --id 0x49dc71ac8bb7b649c8ff21d255295da72d791707

----- Move Object (0x49dc71ac8bb7b649c8ff21d255295da72d791707[2]) -----
Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )
Version: 2
Storage Rebate: 12
Previous Transaction: kTbgQmSch/kav+EW2abSfM3w4Seujv3UK7Yn4ZUV34Q=
----- Data -----
type: 0xccfefb2987fabb577cb4e5cb726d7d3a08c7f2e2::objid::Cat
id: 0x49dc71ac8bb7b649c8ff21d255295da72d791707

在非常特殊的情况下,这实际上不能被用于攻击链上的任何东西。某些合约必须通过 UID 引用外部对象,并假设其类型,以至于改变它会违反这些假设。

然而,这对假定链上对象类型的链下应用程序可能会造成问题。

使用合约控制隐藏和恢复对象

通常,可以将存储在账户中的 Sui 对象转移到另一个对象中,如果该 Sui 对象具有 store。然而,如果你控制着创建对象的合约,则可以对其进行销毁并将 UID 存储在另一个对象中,即使该对象没有 store

struct Cat has key { // 注意:没有 `store` 能力!
    id: UID,
}

struct Tomb has key {
    id: UID,
    cat_id: UID,
}

public entry fun entomb(cat: Cat, ctx: &mut TxContext) {
    let Cat { // 解构 Cat 以提取其 UID
        id: cat_id
    } = cat;
    let tomb = Tomb {
        id: object::new(ctx),
        cat_id: cat_id,
    };
    transfer::transfer(tomb, tx_context::sender(ctx));
}

public entry fun resurrect(tomb: Tomb, ctx: &mut TxContext) {
    let Tomb { // 从墓碑中获取猫 UID,同时解构墓碑
        id: tomb_id,
        cat_id: cat_id,
    } = tomb;
    object::delete(tomb_id);
    let cat = Cat { // 创建一个新猫,具有与之前相同的 UID
        id: cat_id,
    };
    transfer::transfer(cat, tx_context::sender(ctx));
}

让我们用之前的猫尝试一下。让我们查看一下猫:

$ sui client object --id 0x49dc71ac8bb7b649c8ff21d255295da72d791707

----- Move Object (0x49dc71ac8bb7b649c8ff21d255295da72d791707[2]) -----
Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )
Version: 2
Storage Rebate: 12
Previous Transaction: kTbgQmSch/kav+EW2abSfM3w4Seujv3UK7Yn4ZUV34Q=
----- Data -----
type: 0xccfefb2987fabb577cb4e5cb726d7d3a08c7f2e2::objid::Cat
id: 0x49dc71ac8bb7b649c8ff21d255295da72d791707

现在让我们用 entomb(cat) 隐藏这只猫:

$ sui client call --package 0xccfefb2987fabb577cb4e5cb726d7d3a08c7f2e2 --module objid --gas-budget 10000 --function entomb --args 0x49dc71ac8bb7b649c8ff21d255295da72d791707

----- Certificate ----
<snip>
----- Transaction Effects ----
Status : Success
Created Objects:
  - ID: 0x342fdc3c73baba9d4c7717ff899ef74c3fe73833 , Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )
Mutated Objects:
  - ID: 0x37d38af0f4cac30679658c3a75cbbf90169d82d7 , Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )
Wrapped Objects:
  - ID: 0x49dc71ac8bb7b649c8ff21d255295da72d791707

请注意,现在我们的猫被认为是一个包装对象。我们将检查猫是否无法再通过其 ID 访问:

$ sui client object --id 0x49dc71ac8bb7b649c8ff21d255295da72d791707
Object deleted at reference (0x49dc71ac8bb7b649c8ff21d255295da72d791707, SequenceNumber(3), o#5858585858585858585858585858585858585858585858585858585858585858)。

现在我们通过 resurrect(tomb) 把猫带回来:

$ sui client call --package 0xccfefb2987fabb577cb4e5cb726d7d3a08c7f2e2 --module objid --gas-budget 10000 --function resurrect --args 0x342fdc3c73baba9d4c7717ff899ef74c3fe73833

----- Certificate ----
<snip>
----- Transaction Effects ----
Status : Success
Mutated Objects:
    - ID: 0x37d38af0f4cac30679658c3a75cbbf90169d82d7 , Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )
Deleted Objects:
    - ID: 0x342fdc3c73baba9d4c7717ff899ef74c3fe73833
Unwrapped Objects:
    - ID: 0x49dc71ac8bb7b649c8ff21d255295da72d791707 , Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6

让我们看看这只猫:

$ sui client object --id 0x49dc71ac8bb7b649c8ff21d255295da72d791707

----- Move Object (0x49dc71ac8bb7b649c8ff21d255295da72d791707[4]) -----
Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )
Version: 4
Storage Rebate: 12
Previous Transaction: /u9PrwRWlpM2S31y59iyIEMKHa1/GBNeo5s/b6olZzE=
----- Data -----
type: 0xccfefb2987fabb577cb4e5cb726d7d3a08c7f2e2::objid::Cat
id: 0x49dc71ac8bb7b649c8ff21d255295da72d791707

这在安全上的影响与 UID 交换非常相似。链上程序unlikely会受到漏洞的影响,但链下程序可能会因对象的出现和消失而感到困惑。

更新(2022年12月1日):对象 UID 交换和隐藏实际上是由于 Sui 字节码验证器中的一个错误而可能的。在此帖子发布后 30 分钟后,已经在 这次\ 提交↗ 中进行了修复。我们对 Mysten Labs 团队的迅速响应表示赞赏。

结论

Sui Move 是一个仍在快速发展的新链,但已经具有突破性的性能改进和良好设计的链语言。Sui Move 具有一些可能导致漏洞的不直观行为。作为更广泛的加密生态系统的审计师和构建者,我们将充满期待地关注 Sui。

  • 原文链接: zellic.io/blog/move-fast...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
zellic
zellic
Security reviews and research that keep winners winning. https://www.zellic.io/