围绕Aptos Coin解释Move中的一些特性。
简介
Aptos的数据存储结构 -> 每个地址下都有一个Map来存储不同的module(合约)和resource(data)
Move Struct/Resource中的一些特性 - > abilities和封装性
Aptos Coin -> 结合swap的实现来解释Move基于对象的操作
Signer/Resource Account -> 合约中生成signer对象/Account可以另外的私钥或者是有cap来控制
这里插一嘴Aptos的数据存储结构。

图片拷贝自Github,也是一个比较好的Move文档。
总得来说,存储为:
通过address和版本来索引账户下的存储
账户下的存储结构为 BTreeMap(键值对存储, 这个BTreeMap和 HashMap区别比较大的是,BTreeMap的key是有序的,HashMap里的key是无序的)
BTreeMap中通过不同的key来存储不同的值
主要是分为两类key,modules(合约)类型和resources(数据)类型,key的前缀会不同。不同的modules的key也是不同。
常见的Resources,比如
AccountResource,存储账户的sequence等信息//aptos的地址可以更换authentication_key pub struct AccountResource { authentication_key: Vec<u8>, sequence_number: u64, guid_creation_num: u64, coin_register_events: EventHandle, key_rotation_events: EventHandle, rotation_capability_offer: Option<AccountAddress>, signer_capability_offer: Option<AccountAddress>, }如图所示,不同的coin
账户下的数据的update权限,取决于定义对应Resource的module中提供的方法。(struct对象只有在被定义的module中才可访问到field
Move是基于Rust创建的一门语言。
Move的文档中有很详细的介绍,这里就structs-and-resources有几个比较有意思的点这里拎出来看看。
Move中分module和script, module可以理解成合约,会存储在链上(全局存储中),script是链下编写,编译成bytecode后,随交易一起执行的脚本,script不会存储在链上。交易中可以直接调用module中被entry修饰的方法,module中其余public的方法可以被另外的module或者script调用。
module aptos_framework::coin {
/// Main structure representing a coin/token in an account's custody.
struct Coin<phantom CoinType> has store {
/// Amount of coin this address has.
value: u64,
}
struct WithdrawEvent has drop, store {
amount: u64,
}
}
如上所述,在定义struct时可以定义has store/drop,拢共有4种abilities可以选择
copy赋予可copy的能力,也就是说可以进行值拷贝。
script { use {{sender}}::Country; fun main() { let country = Country::new_country(1, 1000000); let _ = copy country; //copy是关键字,手动调用进行拷贝 } }drop在Rust中,变量在离开作用域时就会自动被destroy,Move中增加了新的特性,只有拥有
drop能力的变量才能被成功destroy(离开作用域)。没有drop能力的变量,如果被destroy,就会abort报错。store可以被存储在全局存储中
key
可以被存储在全局存储中( 类型名会作为key,结构体的值会作为value ) [全局存储是按key-value的形式进行存储]
默认情况下的结构体,意味着不能被复制,不能被删除,不能被存储在全局存储中。只有定义时添加对应的能力,才具备相应的功能。
通过模式匹配(解构)可以销毁结构体的值,这种方式不需要结构体有drop能力。
module aptos_framework::coin {
public fun burn<CoinType>(
coin: Coin<CoinType>,
_cap: &BurnCapability<CoinType>,
) acquires CoinInfo {
//Coin only has store
let Coin { value: amount } = coin;
//解构后,coin已经是被Dropped
}
}
结构类型只能在定义结构的模块内创建和销毁
结构类型只能在定义结构的模块内访问属性字段
结构体声明无public这样的修饰符。整个封装性就限死了。要在模块之外修改值,就需要为他们提供public的方法。
比如很重要的Coin类型。
struct Coin<phantom CoinType> has store {
/// Amount of coin this address has.
value: u64,
}
coin模块中可以new Coin值的方法只有mint方法,外部也没法儿直接修改value的值,只能通过coin模块提供的方法来进行修改。
Coin是最基础的功能,APT也是Coin类型,(ERC20的话,在aptos上也是Coin类型)。源码定义可见 Github。
(在aptos中 coin指的类似"erc20", token指的是nft )
aptos里的coin为定制了一切,包括存储结构和方法。
coin的代码部署在0x01::coin下,可以通过调用0x01::coin下的方法来进行new/transfer/mint/burn 代币
列举一部分全局存储
//coin只有store功能,无drop能力,外部module/script可以将coin对象进行存储,但是不能将coin对象直接destroy
struct Coin<phantom CoinType> has store {
/// Amount of coin this address has.
value: u64,
}
/// A holder of a specific coin types and associated event handles.
/// These are kept in a single resource to ensure locality of data.
struct CoinStore<phantom CoinType> has key {
coin: Coin<CoinType>,
frozen: bool,
deposit_events: EventHandle<DepositEvent>,
withdraw_events: EventHandle<WithdrawEvent>,
}
/// Information about a specific coin type. Stored on the creator of the coin's account.
struct CoinInfo<phantom CoinType> has key {
name: string::String,
/// Symbol of the coin, usually a shorter version of the name.
/// For example, Singapore Dollar is SGD.
symbol: string::String,
/// Number of decimals used to get its user representation.
/// For example, if `decimals` equals `2`, a balance of `505` coins should
/// be displayed to a user as `5.05` (`505 / 10 ** 2`).
decimals: u8,
/// Amount of this coin type in existence.
supply: Option<OptionalAggregator>,
}
比如在浏览器看一个账户下的CoinStore数据

Move可以使用5个指令在全局存储中创建/删除/更新资源。

我们可以通过调用0x01::coin下的方法来进行new/transfer/mint/burn 代币,下面一起看看coin中提供的这几个方法(也可以在官方文档看到这几个方法的介绍)
//通过initialize来new coin, 返回的几个cap,可以看作是几个授权,比如mint的时候需要mint cap, burn的时候需要 burn cap,只有搞到对应的cap才可以进行mint和burn(cap的实现也好有意思,后面讲)
public fun initialize<CoinType>(
account: &signer,
name: string::String,
symbol: string::String,
decimals: u8,
monitor_supply: bool,
): (BurnCapability<CoinType>, FreezeCapability<CoinType>, MintCapability<CoinType>)
//用户想要接收除APTOS默认币以外的代币时,必须先由接受者主动明确的注册register接收这个代币,然后才能接收。(不能随意空投代币,为啥非得 register呢,因为首次创建资源时使用的move_to<T>(&signer,T),参数类型是&signer,必须要用户自己签名
public fun register<CoinType>(account: &signer)
//mint方法,注意需要cap参数(即通过cap来做权限判断)
public fun mint<CoinType>(
amount: u64,
_cap: &MintCapability<CoinType>,
): Coin<CoinType> acquires CoinInfo
//burn方法,也是需要cap
public fun burn<CoinType>(
coin: Coin<CoinType>,
_cap: &BurnCapability<CoinType>,
) acquires CoinInfo
//由entry修饰的transfer方法,可以在tx中直接调用(其他方法都只能在module或者script中进行调用
public entry fun transfer<CoinType>(
from: &signer,
to: address,
amount: u64,
) acquires CoinStore
//transfer其实是deposit+withdraw的封装
public fun deposit<CoinType>(account_addr: address, coin: Coin<CoinType>) acquires CoinStore
public fun withdraw<CoinType>(
account: &signer,
amount: u64,
): Coin<CoinType> acquires CoinStore
方法签名中通过
acquires来声明会读写的resources,但是,initialize和register中都会创建resources,确不用声明,所以可能是如果方法里只是创建的话,就不用声明在方法签名中(冲突的话,创建就会失败mint/withdraw方法的返回值都是
Coin<CoinType>,这是move的特点 - 基于对象操作,在外部module或者script中调用时,无法create/destroy Coin类型,也无法直接访问到coin里的value,必须都通过coin模块提供的方法来访问。所以,换个思路的话,就是对象可以在各处流转,不会消失,不会发生变更。无approve方法
上面提到 Coin对象可以在各处流转,要具体解释的话,我们可以借助swap的实现来聊这个事。
找到一个aptos 的swap,代码在 => liquidswap。后面我们摘关键部分来说。
我们熟知的swap实现,一个LP Pool,提供流动性的时候,资金转到LP pool里,swap的时候,需要的资金从LP Pool兑换出来。在liquidswap中,
结论,用户的资产Coin转到Pool中,存储的是Coin对象,从Pool中转给用户时,将Coin对象直接deposit给用户即可。拿到Coin对象后,使用不需要任何的权限,只有将Coin对象deposit到某个用户下,用户再Withdraw Coin对象时才需要signer权限。
//add add_liquidity 的coin有关实现代码 //忽略x*y=z的相关东西
public entry fun add_liquidity<X, Y, Curve>(
account: &signer,
coin_x_val: u64,
coin_x_val_min: u64,
coin_y_val: u64,
coin_y_val_min: u64,
) {
//0. account是signer,从用户地址下将Coin对象转出,coin_x coin_y都是Coin对象
let coin_x = coin::withdraw<X>(account, coin_x_val);
let coin_y = coin::withdraw<Y>(account, coin_y_val);
//1. LP信息的存储,存储在统一个地址下liquidswap_pool_account
let pool = borrow_global_mut<LiquidityPool<X, Y, Curve>>(@liquidswap_pool_account);
//2.我们先来看看LiquidityPool这个pool对象有什么值
struct LiquidityPool<phantom X, phantom Y, phantom Curve> has key {
coin_x_reserve: Coin<X>, //直接将Coin对象存储下来
coin_y_reserve: Coin<Y>,
//...省略很多
}
//3. 将用户提供流动性的Coin 合并入pool(合并主要的操作是将后者合并入前者,后者对象销毁)
coin::merge(&mut pool.coin_x_reserve, coin_x);
coin::merge(&mut pool.coin_y_reserve, coin_y);
//因为pool是&mut,允许值可变的指针,所以coin_x_reserve/coin_y_reserve的修改就会自动同步到全局存储中
//到此,就完成了将用户的资产存储pool池,在add_liquidity中,存入的资产只会有withdraw事件,不会触发deposit事件,因为把Coin对象先存在pool里,并没有真正把这笔资产转到某个账户名下
//在用户进行swap的时候,需要从pool中转出Coin对象到用户下时,就会这样操作
let x_coin_to_return = coin::extract(&mut pool.coin_x_reserve, x_to_return_val);
coin::deposit(account_addr, x_coin_to_return);
//触发Deposit事件
public fun initialize<CoinType>(
account: &signer,
name: string::String,
symbol: string::String,
decimals: u8,
monitor_supply: bool,
): (BurnCapability<CoinType>, FreezeCapability<CoinType>, MintCapability<CoinType>)
public fun mint<CoinType>(
amount: u64,
_cap: &MintCapability<CoinType>,
): Coin<CoinType> acquires CoinInfo {
//在mint方法内也未使用到_cap
}
/// Capability required to mint coins.
struct MintCapability<phantom CoinType> has copy, store {}
/// Capability required to freeze a coin store.
struct FreezeCapability<phantom CoinType> has copy, store {}
/// Capability required to burn coins.
struct BurnCapability<phantom CoinType> has copy, store {}
可以看到,在调用initialize来创建新的Coin后,返回值是几个Cap对象,然后在mint/burn对应的方法时,需要传入对应的cap对象来进行操作。
比较有意思的是,这里还是利用了封装性,操作对象。下面可以看到MintCapability其实啥属性/方法都没有定义,就是一个空的结构体。因为这个结构体的对象只会在initialize里产生,别的地方无法产生新的对象,所以可以直接使用这样的方式来做权限判断,就像是给他颁布了一个授权对象,他要做什么事,就拿着对应的授权对象 来就行。
甚至,在mint方法内部都不需要判断_cap,因为vm会判断类型,调用mint时只能接受&MintCapability<CoinType>的cap。
然后,我们再以liquidswap中的例子来举例这个cap的用法。在LP中,添加流动性时,需要mint LP Coin给用户,移除流动性的时候,需要burn LP Coin。结合上面Coin对象,LP中也是将cap对象存储了下来。
//上面我们也提到的这个LP存储,这里将所有的field都列出来了
struct LiquidityPool<phantom X, phantom Y, phantom Curve> has key {
coin_x_reserve: Coin<X>, //直接存储x/y 的Coin对象
coin_y_reserve: Coin<Y>,
last_block_timestamp: u64,
last_price_x_cumulative: u128,
last_price_y_cumulative: u128,
lp_mint_cap: coin::MintCapability<LP<X, Y, Curve>>,
lp_burn_cap: coin::BurnCapability<LP<X, Y, Curve>>,
// Scales are pow(10, token_decimals).
x_scale: u64,
y_scale: u64,
locked: bool,
fee: u64, // 1 - 100 (0.01% - 1%)
dao_fee: u64, // 0 - 100 (0% - 100%)
}
//当有新的LP注册时
/// Register liquidity pool `X`/`Y`.
public fun register<X, Y, Curve>(acc: &signer) acquires PoolAccountCapability {
//..省略一些代码
let (lp_burn_cap, lp_freeze_cap, lp_mint_cap) =
coin::initialize<LP<X, Y, Curve>>(
&pool_account,
lp_name,
lp_symbol,
6,
true
);
coin::destroy_freeze_cap(lp_freeze_cap);
let pool = LiquidityPool<X, Y, Curve> {
coin_x_reserve: coin::zero<X>(),
coin_y_reserve: coin::zero<Y>(),
last_block_timestamp: 0,
last_price_x_cumulative: 0,
last_price_y_cumulative: 0,
lp_mint_cap, //将mint/burn cap对象存储下来
lp_burn_cap,
//.. 省略一些属性
};
move_to(&pool_account, pool);
//..省略一些代码
}
//添加流动性的时候,进行mint
let lp_coins = coin::mint<LP<X, Y, Curve>>(provided_liq, &pool.lp_mint_cap);
//移除流动性的时候,进行burn
coin::burn(lp_coins, &pool.lp_burn_cap);
liquidswap 中提供了闪电贷的功能,实现也是相当巧妙。关键点在于Flashloan这个struct没有任何ability,既不能存储,也不能destroy。
外部module/script 通过flashloan方法借到钱和Flashloan对象,你就必须把Flashloan对象还回来。把Flashloan对象还回来的唯一途径就是pay_flashloan,把钱和Flashloan对象都还了。
/// Flash loan resource.
/// There is no way in Move to pass calldata and make dynamic calls, but a resource can be used for this purpose.
/// To make the execution into a single transaction, the flash loan function must return a resource
/// that cannot be copied, cannot be saved, cannot be dropped, or cloned.
struct Flashloan<phantom X, phantom Y, phantom Curve> {
x_loan: u64,
y_loan: u64
}
public fun flashloan<X, Y, Curve>(x_loan: u64, y_loan: u64): (Coin<X>, Coin<Y>, Flashloan<X, Y, Curve>)
acquires LiquidityPool, EventsStore {}
public fun pay_flashloan<X, Y, Curve>(
x_in: Coin<X>,
y_in: Coin<Y>,
loan: Flashloan<X, Y, Curve>
) acquires LiquidityPool, EventsStore {
//... 通过解构完成对象的destroy
let Flashloan { x_loan, y_loan } = loan;
}
在Swap的实现中,有这样的一个需求,注册新的交易对时,需要创建新的LP Coin,创建新的Coin,是调用coin::initialize方法,再来看看coin::initialize的方法签名。
public fun initialize<CoinType>(
account: &signer,
name: string::String,
symbol: string::String,
decimals: u8,
monitor_supply: bool,
)
创建新的Coin,需要signer,并且新创建的CoinInfo将存储在signer下。
那么,合约中有没有办法产生signer呢?有的。
在aptos_framework::account下,我们可以创建无私钥的account(resource account)。
//新account = hash(source+seed), 返回SignerCapability
public fun create_resource_account(source: &signer, seed: vector<u8>): (signer, SignerCapability) acquires Account
//通过SignerCapability就可以生成控制account的signer
public fun create_signer_with_capability(capability: &SignerCapability): signer {
let addr = &capability.account;
create_signer(*addr)
}
struct SignerCapability has drop, store { account: address }
可以看到,我们在程序中创建新的resource account,并且将SignerCapability存储下来,这样就能在程序中产生signer。比如还是那个liquidswap中
//swap合约部署后就创建好liquidswap_account
let (lp_acc, signer_cap) =
account::create_resource_account(liquidswap_admin, b"liquidswap_account_seed");
move_to(liquidswap_admin, PoolAccountCapability { signer_cap });
//新的swap对进行注册时
public fun register<X, Y, Curve>(acc: &signer) acquires PoolAccountCapability {
let pool_cap = borrow_global<PoolAccountCapability>(@liquidswap);
let pool_account = account::create_signer_with_capability(&pool_cap.signer_cap);
//获得lp name/symbol
let (lp_name, lp_symbol) = coin_helper::generate_lp_name_and_symbol<X, Y, Curve>();
// 创建新的LP coin
let (lp_burn_cap, lp_freeze_cap, lp_mint_cap) =
coin::initialize<LP<X, Y, Curve>>(
&pool_account,
lp_name,
lp_symbol,
6,
true
);
}
本质上,resource account和普通account是相同的,都是Account类型的对象,不同的是控制权限不同,使用场景可以更丰富。比如,创建额外的resource account来管理LP,地址不与创建者地址相同。
Liquid swap,所有的LP Coin Info和Pool信息都存储在那个无私钥的resource account下。可以在浏览器上看看效果
附录
swap的合约地址 https://github.com/pontem-network/liquidswap/blob/devnet/Move.toml
