# aptos 合约入门一览 **Published by:** [zoie](https://paragraph.com/@zoie/) **Published on:** 2022-11-10 **URL:** https://paragraph.com/@zoie/aptos-2 ## Content 围绕Aptos Coin解释Move中的一些特性。 简介Aptos的数据存储结构 -> 每个地址下都有一个Map来存储不同的module(合约)和resource(data)Move Struct/Resource中的一些特性 - > abilities和封装性Aptos Coin -> 结合swap的实现来解释Move基于对象的操作Signer/Resource Account -> 合约中生成signer对象/Account可以另外的私钥或者是有cap来控制数据存储结构这里插一嘴Aptos的数据存储结构。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中才可访问到fieldMoveMove是基于Rust创建的一门语言。 Move的文档中有很详细的介绍,这里就structs-and-resources有几个比较有意思的点这里拎出来看看。 Move中分module和script, module可以理解成合约,会存储在链上(全局存储中),script是链下编写,编译成bytecode后,随交易一起执行的脚本,script不会存储在链上。交易中可以直接调用module中被entry修饰的方法,module中其余public的方法可以被另外的module或者script调用。Structmodule 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模块提供的方法来进行修改。CoinCoin是最基础的功能,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,这是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的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下。可以在浏览器上看看效果主网 LP resource accountTestnet LP resource account附录 swap的合约地址 https://github.com/pontem-network/liquidswap/blob/devnet/Move.toml ## Publication Information - [zoie](https://paragraph.com/@zoie/): Publication homepage - [All Posts](https://paragraph.com/@zoie/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@zoie): Subscribe to updates