aptos 合约入门一览

围绕Aptos Coin解释Move中的一些特性。

简介

  • Aptos的数据存储结构 -> 每个地址下都有一个Map来存储不同的module(合约)和resource(data)

  • Move Struct/Resource中的一些特性 - > abilities和封装性

  • Aptos Coin -> 结合swap的实现来解释Move基于对象的操作

  • Signer/Resource Account -> 合约中生成signer对象/Account可以另外的私钥或者是有cap来控制

数据存储结构

这里插一嘴Aptos的数据存储结构。

image
image

图片拷贝自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

Move是基于Rust创建的一门语言。

Move的文档中有很详细的介绍,这里就structs-and-resources有几个比较有意思的点这里拎出来看看。

Move中分module和script, module可以理解成合约,会存储在链上(全局存储中),script是链下编写,编译成bytecode后,随交易一起执行的脚本,script不会存储在链上。交易中可以直接调用module中被entry修饰的方法,module中其余public的方法可以被另外的module或者script调用。

Struct

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,
    }
}
abilities

如上所述,在定义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

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数据

post image

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

post image

方法

我们可以通过调用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对象

上面提到 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事件
Mint/Burn/Freeze Capability
    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;
    }

Signer

在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