# aptos 合约入门一览

By [zoie](https://paragraph.com/@zoie) · 2022-11-10

---

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

简介

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

### 数据存储结构

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

![image](https://storage.googleapis.com/papyrus_images/fab0f095e342897c7b74252d4af5f867151a058e73387751a29685169a0eee95.png)

image

图片拷贝自[Github](https://github.com/move-dao/move_tutorial/blob/main/src/SUMMARY.md)，也是一个比较好的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的[文档](https://diem.github.io/move/introduction.html)中有很详细的介绍，这里就`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](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/sources/coin.move)。

（在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>,
        }
    

比如在[浏览器](https://explorer.aptoslabs.com/account/0xac8c9af888c74b8973d119b0bb4fa784d646794537ef1cee0bc10a36a0bb6623)看一个账户下的CoinStore数据

![](https://storage.googleapis.com/papyrus_images/b866a12197ba5dece5925e0f136e95b40cc32ea24cd604ec59a94738969ecc69.png)

Move可以使用5个指令在[全局存储中创建/删除/更新资源](https://diem.github.io/move/global-storage-operators.html)。

![](https://storage.googleapis.com/papyrus_images/52ad7df82cade13208b3cfa3ef8a67aef3e204c065365072b39cf4bcf24c312b.png)

#### 方法

我们可以通过调用`0x01::coin`下的方法来进行new/transfer/mint/burn 代币，下面一起看看coin中提供的这几个方法(也可以在官[方文档](https://aptos.dev/tutorials/your-first-coin#step-43-understanding-coins)看到这几个方法的介绍)

       //通过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](https://github.com/pontem-network/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](https://github.com/pontem-network/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](https://github.com/pontem-network/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](https://github.com/pontem-network/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下。可以在浏览器上看看效果

*   [主网 LP resource account](https://explorer.aptoslabs.com/account/0x05a97986a9d031c4567e15b797be516910cfcb4156312482efc6a19c0a30c948)
    
*   [Testnet LP resource account](https://explorer.aptoslabs.com/account/0x385068db10693e06512ed54b1e6e8f1fb9945bb7a78c28a45585939ce953f99e)
    

附录

swap的合约地址 [https://github.com/pontem-network/liquidswap/blob/devnet/Move.toml](https://github.com/pontem-network/liquidswap/blob/devnet/Move.toml)

---

*Originally published on [zoie](https://paragraph.com/@zoie/aptos-2)*
