Sui 实用学习笔记 (2) - Move
Sui Move 智能合约相关知识与代码示例
Move 语言
参考资料:
- Move package 编写
- Counter 合约示例
- Move 编程语言中文书
- Move Book 中文版
- 基于虚拟机源码分析move合约系列
- Move语言安全分析
- Sui 官方课程
- BlockEden Sui Move Docs
- 《Move中的设计模式》系列文章
注:以下均为 Sui 链的 Move 合约约定。与 Aptos 虽然均使用 Move 合约,但略有不同。
Move 语言一些特点:
- 基础类型包括 bool, u8, u64, u128, address。不支持有符号数,但可以通过一些 lib 实现 i32, i64, i256, u256 类型。
- 初始化方法名必须为
init
,且最后一个参数类型为&TxContext
。 - Sui 函数支持泛型,可以将类型作为一种参数传入。
- 函数可见性:entry 修饰的方法表示可以由 PTB 发起。此类方法返回类型必须有
drop
。public 方法可以由 PTB 发起,也可以由其他 module 发起。
Object 的引入是 Sui 与以太坊架构上最主要的区别之一。在合约语言层面上,使用 struct 定义 object。Move 合约主体(即 package)不存储任何数据,所有数据都存储在 object 中。因此在 Move 编程中不存在全局变量之类的东西,所有数据都由参数以 object 的形式传入。
前面简单介绍了 object 所有权,这里补充 Move 层面的一些内容:
- address-owned objects 可由特定地址修改(即 owner 发起的交易才能修改对应的 object)。并可通过
sui::transfer::transfer
转移所有权。有 store 标志的 object 可以使用sui::transfer::public_transfer
。 注意 object 创建并设置 owner 后不可以被 share。address-owned objects 由于不可能被其他地址修改,因此相关的交易可以并行处理,利于提高链的性能。 - Coin 是非常常用的 address-owned object。通过
sui::transfer::public_transfer
转移所有权实现转账。 sui::transfer::public_freeze_object
可创建 immutable object。任何人都可以使用。但不能通过& mut
的形式引用,因为不可修改。- object 可以通过嵌套定义 wrap 到其他 object structure 中。此时所有权跟随外层 object。并此内层 object 不再能够通过 id 访问到。
- object 也可以通过
sui::transfer::transfer/public_transfer
直接转移给其他 object id。这样所有权跟随 parent object。
语言层定义 object 时有能力(术语为 ability
)的概念:
- copy :允许此类型的值被复制
- drop :允许此类型的值被弹出/丢弃
- store: 允许此类型的值存在于全局存储的某个结构体中
- 有
store
ability 的 object 可以通过sui::transfer::public_transfer
转移所有权,并且可以 wrap 到其他 object 中。 - 没有
store
则只有定义该 object 的 module 可以使用sui::transfer::transfer
才转移。
- 有
- key: 允许此类型作为全局存储中的键(具有 key 能力的类型才能保存到全局存储中)。有
key
标识的 object 可通过sui::transfer::share_object
进行 share,使任何人均可修改
基础数据类型(包括 bool, u8, u64, u128, address)具有 copy, drop, store 能力。
示例:Counter
示例来自官方文档。
代码如下:
// counter.move
// <Package 名> :: <Module 名>
module counter::counter {
// 导入变量
use sui::transfer;
use sui::object::{Self, UID};
use sui::tx_context::{Self, TxContext};
/// A shared counter.
struct Counter has key {
id: UID,
owner: address, // 注意这个只是 object 里的一个 field,和其自身的 owner 要有所区分。
value: u64
}
/// Create and share a Counter object.
public fun create(ctx: &mut TxContext) {
// share 后 object 可以被任何人操作。
transfer::share_object(Counter {
id: object::new(ctx),
owner: tx_context::sender(ctx), // 相当于 msg.sender
value: 0
})
}
/// Increment a counter by 1.
// 调用方需要传入事先确定 object id,并传入
public fun increment(counter: &mut Counter) {
// 与 Rust 类似的,mut 变量才能被修改。
counter.value = counter.value + 1;
}
/// Set value (only runnable by the Counter owner)
public fun set_value(counter: &mut Counter, value: u64, ctx: &TxContext) {
// 检查 owner
assert!(counter.owner == tx_context::sender(ctx), 0);
counter.value = value;
}
}
示例:Coin
示例来自官方文档。
module examples::mycoin {
use std::option;
use sui::coin;
use sui::transfer;
use sui::tx_context::{Self, TxContext};
/// The type identifier of coin. The coin will have a type
/// tag of kind: `Coin<package_object::mycoin::MYCOIN>`
/// Make sure that the name of the type matches the module's name.
struct MYCOIN has drop {}
/// Module initializer is called once on module publish. A treasury
/// cap is sent to the publisher, who then controls minting and burning
fun init(witness: MYCOIN, ctx: &mut TxContext) {
let (treasury, metadata) = coin::create_currency(witness, 6, b"MYCOIN", b"", b"", option::none(), ctx);
transfer::public_freeze_object(metadata);
transfer::public_transfer(treasury, tx_context::sender(ctx))
}
}
上面的 MYCOIN struct 称为 One-Time Witness(OTW),此类型只有一个实例。Move 定义此种类型的约定为
- 定义需要与 module 一致。
- 有且只有
drop
ability. - struct 中没有其他域,或只有一个 bool。
最终创建的 token object 类型为 sui::coin::Coin<examples::mycoin::MYCOIN>
(注意,链上看的时候 sui 和 examples 都会替换成对应的 object id)。在 Sui 上发 Token 相比于部署一个新合约,更像是创建了一种新类型。Sui 中的类型与 module 相关联,意味着只有 module 的函数才能创建对应类型的 object。
https://suivision.xyz/coins 可以查看 Sui 上发布的 Coin。用户持有的是 sui::coin::Coin
,链上表示 Coin 的一些元数据的是 sui::coin::CoinMetadata
。比如 USDC 的元数据对象的类型是 0x2::coin::CoinMetadata<0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf::coin::COIN>
。
在sui::coin package 中可以找到相应的定义
/// A coin of type `T` worth `value`. Transferable and storable
struct Coin<phantom T> has key, store {
id: UID,
balance: Balance<T>
}
/// Each Coin type T created through `create_currency` function will have a
/// unique instance of CoinMetadata<T> that stores the metadata for this coin type.
struct CoinMetadata<phantom T> has key, store {
id: UID,
/// Number of decimal places the coin uses.
/// A coin with `value ` N and `decimals` D should be shown as N / 10^D
/// E.g., a coin with `value` 7002 and decimals 3 should be displayed as 7.002
/// This is metadata for display usage only.
decimals: u8,
/// Name for the token
name: string::String,
/// Symbol for the token
symbol: ascii::String,
/// Description of the token
description: string::String,
/// URL for the token logo
icon_url: Option<Url>
}
/// Capability allowing the bearer to mint and burn
/// coins of type `T`. Transferable
struct TreasuryCap<phantom T> has key, store {
id: UID,
total_supply: Supply<T>
}
create_currency
代码如下,非常简单,就是创建了 TreasuryCap 和 CoinMetadata 两个 object。
/// Create a new currency type `T` as and return the `TreasuryCap` for
/// `T` to the caller. Can only be called with a `one-time-witness`
/// type, ensuring that there's only one `TreasuryCap` per `T`.
public fun create_currency<T: drop>(
witness: T,
decimals: u8,
symbol: vector<u8>,
name: vector<u8>,
description: vector<u8>,
icon_url: Option<Url>,
ctx: &mut TxContext
): (TreasuryCap<T>, CoinMetadata<T>) {
// Make sure there's only one instance of the type T
assert!(sui::types::is_one_time_witness(&witness), EBadWitness);
(
TreasuryCap {
id: object::new(ctx),
total_supply: balance::create_supply(witness)
},
CoinMetadata {
id: object::new(ctx),
decimals,
name: string::utf8(name),
symbol: ascii::string(symbol),
description: string::utf8(description),
icon_url
}
)
}
mint 方法如下,只有持有对应 TreasuryCap object 的地址才能调用 mint。mint 创建出对应的 Coin object 返回。
/// Create a coin worth `value`. and increase the total supply
/// in `cap` accordingly.
public fun mint<T>(
cap: &mut TreasuryCap<T>, value: u64, ctx: &mut TxContext,
): Coin<T> {
Coin {
id: object::new(ctx),
balance: balance::increase_supply(&mut cap.total_supply, value)
}
}
这里是 Sui 中的一种典型 Pattern,即通过调用方是否有某一类型的 object 来进行鉴权。下面也是一个例子。效果相当于 Solidity 中的 onlyOwner。
/// Update name of the coin in `CoinMetadata`
public entry fun update_name<T>(
_treasury: &TreasuryCap<T>, metadata: &mut CoinMetadata<T>, name: string::String
) {
metadata.name = name;
}
示例:Flashloan
示例来自 Cetus Protocol
// 注意这个 struct 没有 key, drop 等能力
struct FlashSwapReceipt<phantom CoinTypeA, phantom CoinTypeB> {
pool_id: ID,
a2b: bool,
pay_amount: u64,
protocol_fee_amount: u64,
}
public(friend) fun flash_swap<CoinTypeA, CoinTypeB>(
pool: &mut Pool<CoinTypeA, CoinTypeB>,
amount_in: u64,
amount_out: u64,
a2b: bool,
): (Balance<CoinTypeA>, Balance<CoinTypeB>, FlashSwapReceipt<CoinTypeA, CoinTypeB>) {
// 执行转账逻辑
}
public(friend) fun repay_flash_swap<CoinTypeA, CoinTypeB>(
pool: &mut Pool<CoinTypeA, CoinTypeB>,
balance_a: Balance<CoinTypeA>,
balance_b: Balance<CoinTypeB>,
receipt: FlashSwapReceipt<CoinTypeA, CoinTypeB>
) {
// 执行还款逻辑
}
基本思路是定义一个没有任何能力的 struct(称为 Hot Potato )。并在借款函数中的返回。由于该 type 的对象不能被保存在 storage 中,也不能被丢弃,想要交易成功,就必须调用 repay_flash_swap 传回合约处理。