From 88ad9a1132c61f4633336581727c7ea01dd0757b Mon Sep 17 00:00:00 2001 From: runtianz Date: Mon, 9 Sep 2024 10:02:05 -0700 Subject: [PATCH] add permissions for fungible assets operation --- .../aptos-framework/doc/aptos_account.md | 1 + .../framework/aptos-framework/doc/coin.md | 145 +++++++-------- .../doc/dispatchable_fungible_asset.md | 1 + .../aptos-framework/doc/fungible_asset.md | 172 ++++++++++++++++++ .../sources/aptos_account.move | 1 + .../aptos-framework/sources/coin.move | 19 ++ .../sources/dispatchable_fungible_asset.move | 1 + .../sources/fungible_asset.move | 133 +++++++++++++- 8 files changed, 392 insertions(+), 81 deletions(-) diff --git a/aptos-move/framework/aptos-framework/doc/aptos_account.md b/aptos-move/framework/aptos-framework/doc/aptos_account.md index 33cd0ff87e5a13..6b4c97226eece0 100644 --- a/aptos-move/framework/aptos-framework/doc/aptos_account.md +++ b/aptos-move/framework/aptos-framework/doc/aptos_account.md @@ -605,6 +605,7 @@ to transfer APT) - if we want to allow APT PFS without account itself // as APT cannot be frozen or have dispatch, and PFS cannot be transfered // (PFS could potentially be burned. regular transfer would permanently unburn the store. // Ignoring the check here has the equivalent of unburning, transfers, and then burning again) + fungible_asset::withdraw_permission_check_by_address(source, @aptos_fungible_asset, amount); fungible_asset::deposit_internal(recipient_store, fungible_asset::withdraw_internal(sender_store, amount)); } diff --git a/aptos-move/framework/aptos-framework/doc/coin.md b/aptos-move/framework/aptos-framework/doc/coin.md index 07e97171f99b19..93c6b94cdc8f5f 100644 --- a/aptos-move/framework/aptos-framework/doc/coin.md +++ b/aptos-move/framework/aptos-framework/doc/coin.md @@ -158,6 +158,7 @@ This module provides the foundation for typesafe Coins. use 0x1::object; use 0x1::option; use 0x1::optional_aggregator; +use 0x1::permissioned_signer; use 0x1::primary_fungible_store; use 0x1::signer; use 0x1::string; @@ -3423,6 +3424,13 @@ Withdraw specified amount of coin CoinType from the si ): Coin<CoinType> acquires CoinStore, CoinConversionMap, CoinInfo, PairedCoinType { let account_addr = signer::address_of(account); + let metadata = paired_metadata<CoinType>(); + if(option::is_some(&metadata)) { + fungible_asset::withdraw_permission_check_by_address(account, object::object_address(&option::destroy_some(metadata)), amount); + } else { + permissioned_signer::assert_master_signer(account); + }; + let (coin_amount_to_withdraw, fa_amount_to_withdraw) = calculate_amount_to_withdraw<CoinType>( account_addr, amount @@ -3814,64 +3822,6 @@ initialize, initialize_internal, initialize_with_parallelizable_supply; - - - - -
fun spec_is_account_registered<CoinType>(account_addr: address): bool {
-   let paired_metadata_opt = spec_paired_metadata<CoinType>();
-   exists<CoinStore<CoinType>>(account_addr) || (option::spec_is_some(
-       paired_metadata_opt
-   ) && primary_fungible_store::spec_primary_store_exists(account_addr, option::spec_borrow(paired_metadata_opt)))
-}
-
- - - - - - - -
schema CoinSubAbortsIf<CoinType> {
-    amount: u64;
-    let addr = type_info::type_of<CoinType>().account_address;
-    let maybe_supply = global<CoinInfo<CoinType>>(addr).supply;
-    include (option::is_some(
-        maybe_supply
-    )) ==> optional_aggregator::SubAbortsIf { optional_aggregator: option::borrow(maybe_supply), value: amount };
-}
-
- - - - - - - -
schema CoinAddAbortsIf<CoinType> {
-    amount: u64;
-    let addr = type_info::type_of<CoinType>().account_address;
-    let maybe_supply = global<CoinInfo<CoinType>>(addr).supply;
-    include (option::is_some(
-        maybe_supply
-    )) ==> optional_aggregator::AddAbortsIf { optional_aggregator: option::borrow(maybe_supply), value: amount };
-}
-
- - - - - - - -
schema AbortsIfNotExistCoinInfo<CoinType> {
-    let addr = type_info::type_of<CoinType>().account_address;
-    aborts_if !exists<CoinInfo<CoinType>>(addr);
-}
-
- - - ### Struct `AggregatableCoin` @@ -4215,6 +4165,64 @@ Get address by reflection. + + + + +
fun spec_is_account_registered<CoinType>(account_addr: address): bool {
+   let paired_metadata_opt = spec_paired_metadata<CoinType>();
+   exists<CoinStore<CoinType>>(account_addr) || (option::spec_is_some(
+       paired_metadata_opt
+   ) && primary_fungible_store::spec_primary_store_exists(account_addr, option::spec_borrow(paired_metadata_opt)))
+}
+
+ + + + + + + +
schema CoinSubAbortsIf<CoinType> {
+    amount: u64;
+    let addr = type_info::type_of<CoinType>().account_address;
+    let maybe_supply = global<CoinInfo<CoinType>>(addr).supply;
+    include (option::is_some(
+        maybe_supply
+    )) ==> optional_aggregator::SubAbortsIf { optional_aggregator: option::borrow(maybe_supply), value: amount };
+}
+
+ + + + + + + +
schema CoinAddAbortsIf<CoinType> {
+    amount: u64;
+    let addr = type_info::type_of<CoinType>().account_address;
+    let maybe_supply = global<CoinInfo<CoinType>>(addr).supply;
+    include (option::is_some(
+        maybe_supply
+    )) ==> optional_aggregator::AddAbortsIf { optional_aggregator: option::borrow(maybe_supply), value: amount };
+}
+
+ + + + + + + +
schema AbortsIfNotExistCoinInfo<CoinType> {
+    let addr = type_info::type_of<CoinType>().account_address;
+    aborts_if !exists<CoinInfo<CoinType>>(addr);
+}
+
+ + + ### Function `name` @@ -4595,27 +4603,6 @@ The creator of CoinType must be @aptos_framework. -Make sure name and symbol are legal length. -Only the creator of CoinType can initialize. - - - - - -
schema InitializeInternalSchema<CoinType> {
-    account: signer;
-    name: vector<u8>;
-    symbol: vector<u8>;
-    let account_addr = signer::address_of(account);
-    let coin_address = type_info::type_of<CoinType>().account_address;
-    aborts_if coin_address != account_addr;
-    aborts_if exists<CoinInfo<CoinType>>(account_addr);
-    aborts_if len(name) > MAX_COIN_NAME_LENGTH;
-    aborts_if len(symbol) > MAX_COIN_SYMBOL_LENGTH;
-}
-
- - diff --git a/aptos-move/framework/aptos-framework/doc/dispatchable_fungible_asset.md b/aptos-move/framework/aptos-framework/doc/dispatchable_fungible_asset.md index b3b76f108128ca..da71484cc0c008 100644 --- a/aptos-move/framework/aptos-framework/doc/dispatchable_fungible_asset.md +++ b/aptos-move/framework/aptos-framework/doc/dispatchable_fungible_asset.md @@ -220,6 +220,7 @@ The semantics of deposit will be governed by the function specified in DispatchF amount: u64, ): FungibleAsset acquires TransferRefStore { fungible_asset::withdraw_sanity_check(owner, store, false); + fungible_asset::withdraw_permission_check(owner, store, amount); let func_opt = fungible_asset::withdraw_dispatch_function(store); if (option::is_some(&func_opt)) { assert!( diff --git a/aptos-move/framework/aptos-framework/doc/fungible_asset.md b/aptos-move/framework/aptos-framework/doc/fungible_asset.md index f12ee14942b4f4..b3a42cd2297ba0 100644 --- a/aptos-move/framework/aptos-framework/doc/fungible_asset.md +++ b/aptos-move/framework/aptos-framework/doc/fungible_asset.md @@ -20,6 +20,7 @@ metadata object can be any object that equipped with use 0x1::function_info; use 0x1::object; use 0x1::option; +use 0x1::permissioned_signer; use 0x1::signer; use 0x1::string; @@ -557,6 +563,33 @@ MutateMetadataRef can be used to directly modify the fungible asset's Metadata. + + + + +## Struct `WithdrawPermission` + + + +
struct WithdrawPermission has copy, drop, store
+
+ + + +
+Fields + + +
+
+metadata_address: address +
+
+ +
+
+ +
@@ -1135,6 +1168,16 @@ Provided withdraw function type doesn't meet the signature requirement. + + +signer don't have the permission to perform withdraw operation + + +
const EWITHDRAW_PERMISSION_DENIED: u64 = 34;
+
+ + + @@ -2675,12 +2718,75 @@ Withdraw amount of the fungible asset from store by th amount: u64, ): FungibleAsset acquires FungibleStore, DispatchFunctionStore, ConcurrentFungibleBalance { withdraw_sanity_check(owner, store, true); + withdraw_permission_check(owner, store, amount); withdraw_internal(object::object_address(&store), amount) } + + + + +## Function `withdraw_permission_check` + +Check the permission for withdraw operation. + + +
public(friend) fun withdraw_permission_check<T: key>(owner: &signer, store: object::Object<T>, amount: u64)
+
+ + + +
+Implementation + + +
public(friend) fun withdraw_permission_check<T: key>(
+    owner: &signer,
+    store: Object<T>,
+    amount: u64,
+) acquires FungibleStore {
+    assert!(permissioned_signer::check_permission(owner, amount as u256, WithdrawPermission {
+        metadata_address: object::object_address(&borrow_store_resource(&store).metadata)
+    }), error::permission_denied(EWITHDRAW_PERMISSION_DENIED));
+}
+
+ + + +
+ + + +## Function `withdraw_permission_check_by_address` + +Check the permission for withdraw operation. + + +
public(friend) fun withdraw_permission_check_by_address(owner: &signer, metadata_address: address, amount: u64)
+
+ + + +
+Implementation + + +
public(friend) fun withdraw_permission_check_by_address(
+    owner: &signer,
+    metadata_address: address,
+    amount: u64,
+) {
+    assert!(permissioned_signer::check_permission(owner, amount as u256, WithdrawPermission {
+        metadata_address,
+    }), error::permission_denied(EWITHDRAW_PERMISSION_DENIED));
+}
+
+ + +
@@ -3672,6 +3778,72 @@ Ensure a known + +## Function `grant_permission` + +Permission management + +Master signer grant permissioned signer ability to withdraw a given amount of fungible asset. + + +
public fun grant_permission(master: &signer, permissioned: &signer, token_type: object::Object<fungible_asset::Metadata>, amount: u64)
+
+ + + +
+Implementation + + +
public fun grant_permission(
+    master: &signer,
+    permissioned: &signer,
+    token_type: Object<Metadata>,
+    amount: u64
+) {
+    permissioned_signer::authorize(
+        master,
+        permissioned,
+        amount as u256,
+        WithdrawPermission {
+            metadata_address: object::object_address(&token_type),
+        }
+    )
+}
+
+ + + +
+ + + +## Function `revoke_permission` + +Removing permissions from permissioned signer. + + +
public fun revoke_permission(permissioned: &signer, token_type: object::Object<fungible_asset::Metadata>)
+
+ + + +
+Implementation + + +
public fun revoke_permission(permissioned: &signer, token_type: Object<Metadata>) {
+    permissioned_signer::revoke_permission(permissioned, WithdrawPermission {
+        metadata_address: object::object_address(&token_type),
+    })
+}
+
+ + +
diff --git a/aptos-move/framework/aptos-framework/sources/aptos_account.move b/aptos-move/framework/aptos-framework/sources/aptos_account.move index 34addca77cf286..e13a84be4772f7 100644 --- a/aptos-move/framework/aptos-framework/sources/aptos_account.move +++ b/aptos-move/framework/aptos-framework/sources/aptos_account.move @@ -210,6 +210,7 @@ module aptos_framework::aptos_account { // as APT cannot be frozen or have dispatch, and PFS cannot be transfered // (PFS could potentially be burned. regular transfer would permanently unburn the store. // Ignoring the check here has the equivalent of unburning, transfers, and then burning again) + fungible_asset::withdraw_permission_check_by_address(source, @aptos_fungible_asset, amount); fungible_asset::deposit_internal(recipient_store, fungible_asset::withdraw_internal(sender_store, amount)); } diff --git a/aptos-move/framework/aptos-framework/sources/coin.move b/aptos-move/framework/aptos-framework/sources/coin.move index 91a54edb7fdddb..b134d36d8c6667 100644 --- a/aptos-move/framework/aptos-framework/sources/coin.move +++ b/aptos-move/framework/aptos-framework/sources/coin.move @@ -13,6 +13,7 @@ module aptos_framework::coin { use aptos_framework::event::{Self, EventHandle}; use aptos_framework::guid; use aptos_framework::optional_aggregator::{Self, OptionalAggregator}; + use aptos_framework::permissioned_signer; use aptos_framework::system_addresses; use aptos_framework::fungible_asset::{Self, FungibleAsset, Metadata, MintRef, TransferRef, BurnRef}; @@ -1168,11 +1169,29 @@ module aptos_framework::coin { ): Coin acquires CoinStore, CoinConversionMap, CoinInfo, PairedCoinType { let account_addr = signer::address_of(account); + let metadata = paired_metadata(); + if(option::is_some(&metadata)) { + fungible_asset::withdraw_permission_check_by_address(account, object::object_address(&option::destroy_some(metadata)), amount); + } else { + permissioned_signer::assert_master_signer(account); + }; + let (coin_amount_to_withdraw, fa_amount_to_withdraw) = calculate_amount_to_withdraw( account_addr, amount ); let withdrawn_coin = if (coin_amount_to_withdraw > 0) { + let metadata = paired_metadata(); + if(option::is_some(&metadata)) { + fungible_asset::withdraw_permission_check_by_address( + account, + object::object_address(&option::destroy_some(metadata)), + coin_amount_to_withdraw + ); + } else { + permissioned_signer::assert_master_signer(account); + }; + let coin_store = borrow_global_mut>(account_addr); assert!( !coin_store.frozen, diff --git a/aptos-move/framework/aptos-framework/sources/dispatchable_fungible_asset.move b/aptos-move/framework/aptos-framework/sources/dispatchable_fungible_asset.move index 5a70aff95d2c11..37c16214fd8795 100644 --- a/aptos-move/framework/aptos-framework/sources/dispatchable_fungible_asset.move +++ b/aptos-move/framework/aptos-framework/sources/dispatchable_fungible_asset.move @@ -77,6 +77,7 @@ module aptos_framework::dispatchable_fungible_asset { amount: u64, ): FungibleAsset acquires TransferRefStore { fungible_asset::withdraw_sanity_check(owner, store, false); + fungible_asset::withdraw_permission_check(owner, store, amount); let func_opt = fungible_asset::withdraw_dispatch_function(store); if (option::is_some(&func_opt)) { assert!( diff --git a/aptos-move/framework/aptos-framework/sources/fungible_asset.move b/aptos-move/framework/aptos-framework/sources/fungible_asset.move index 946d7b05eb415c..a97c7710680c9a 100644 --- a/aptos-move/framework/aptos-framework/sources/fungible_asset.move +++ b/aptos-move/framework/aptos-framework/sources/fungible_asset.move @@ -6,6 +6,7 @@ module aptos_framework::fungible_asset { use aptos_framework::event; use aptos_framework::function_info::{Self, FunctionInfo}; use aptos_framework::object::{Self, Object, ConstructorRef, DeleteRef, ExtendRef}; + use aptos_framework::permissioned_signer::{Self, Permission}; use std::string; use std::features; @@ -87,7 +88,8 @@ module aptos_framework::fungible_asset { const ECONCURRENT_BALANCE_NOT_ENABLED: u64 = 32; /// Provided derived_supply function type doesn't meet the signature requirement. const EDERIVED_SUPPLY_FUNCTION_SIGNATURE_MISMATCH: u64 = 33; - + /// signer don't have the permission to perform withdraw operation + const EWITHDRAW_PERMISSION_DENIED: u64 = 34; // // Constants // @@ -194,6 +196,10 @@ module aptos_framework::fungible_asset { metadata: Object } + struct WithdrawPermission has copy, drop, store { + metadata_address: address, + } + #[event] /// Emitted when fungible assets are deposited into a store. struct Deposit has drop, store { @@ -785,16 +791,66 @@ module aptos_framework::fungible_asset { amount: u64, ): FungibleAsset acquires FungibleStore, DispatchFunctionStore, ConcurrentFungibleBalance { withdraw_sanity_check(owner, store, true); + withdraw_permission_check(owner, store, amount); + withdraw_internal(object::object_address(&store), amount) + } + + public fun withdraw_with_permission( + perm: &mut Permission, + store: Object, + amount: u64, + ): FungibleAsset acquires FungibleStore, DispatchFunctionStore, ConcurrentFungibleBalance { + withdraw_sanity_check_impl(permissioned_signer::address_of(perm), store, true); + assert!( + permissioned_signer::consume_permission(perm, amount as u256, WithdrawPermission { + metadata_address: object::object_address(&borrow_store_resource(&store).metadata) + }), + error::permission_denied(EWITHDRAW_PERMISSION_DENIED) + ); withdraw_internal(object::object_address(&store), amount) } + /// Check the permission for withdraw operation. + public(friend) fun withdraw_permission_check( + owner: &signer, + store: Object, + amount: u64, + ) acquires FungibleStore { + assert!(permissioned_signer::check_permission(owner, amount as u256, WithdrawPermission { + metadata_address: object::object_address(&borrow_store_resource(&store).metadata) + }), error::permission_denied(EWITHDRAW_PERMISSION_DENIED)); + } + + /// Check the permission for withdraw operation. + public(friend) fun withdraw_permission_check_by_address( + owner: &signer, + metadata_address: address, + amount: u64, + ) { + assert!(permissioned_signer::check_permission(owner, amount as u256, WithdrawPermission { + metadata_address, + }), error::permission_denied(EWITHDRAW_PERMISSION_DENIED)); + } + /// Check the permission for withdraw operation. public(friend) fun withdraw_sanity_check( owner: &signer, store: Object, abort_on_dispatch: bool, ) acquires FungibleStore, DispatchFunctionStore { - assert!(object::owns(store, signer::address_of(owner)), error::permission_denied(ENOT_STORE_OWNER)); + withdraw_sanity_check_impl( + signer::address_of(owner), + store, + abort_on_dispatch, + ) + } + + inline fun withdraw_sanity_check_impl( + owner_address: address, + store: Object, + abort_on_dispatch: bool, + ) acquires FungibleStore, DispatchFunctionStore { + assert!(object::owns(store, owner_address), error::permission_denied(ENOT_STORE_OWNER)); let fa_store = borrow_store_resource(&store); assert!( !abort_on_dispatch || !has_withdraw_dispatch_function(fa_store.metadata), @@ -1179,6 +1235,32 @@ module aptos_framework::fungible_asset { move_to(&object_signer, ConcurrentFungibleBalance { balance }); } + /// Permission management + /// + /// Master signer grant permissioned signer ability to withdraw a given amount of fungible asset. + public fun grant_permission( + master: &signer, + permissioned: &signer, + token_type: Object, + amount: u64 + ) { + permissioned_signer::authorize( + master, + permissioned, + amount as u256, + WithdrawPermission { + metadata_address: object::object_address(&token_type), + } + ) + } + + /// Removing permissions from permissioned signer. + public fun revoke_permission(permissioned: &signer, token_type: Object) { + permissioned_signer::revoke_permission(permissioned, WithdrawPermission { + metadata_address: object::object_address(&token_type), + }) + } + #[test_only] use aptos_framework::account; @@ -1234,6 +1316,9 @@ module aptos_framework::fungible_asset { create_store(&object::create_object_from_account(owner), metadata) } + #[test_only] + use aptos_framework::timestamp; + #[test(creator = @0xcafe)] fun test_metadata_basic_flow(creator: &signer) acquires Metadata, Supply, ConcurrentSupply { let (creator_ref, metadata) = create_test_token(creator); @@ -1541,6 +1626,50 @@ module aptos_framework::fungible_asset { assert!(aggregator_v2::read(&borrow_global(object::object_address(&creator_store)).balance) == 30, 12); } + #[test(creator = @0xcafe, aaron = @0xface)] + fun test_e2e_withdraw_limit( + creator: &signer, + aaron: &signer, + ) acquires FungibleStore, Supply, ConcurrentSupply, DispatchFunctionStore, ConcurrentFungibleBalance { + let aptos_framework = account::create_signer_for_test(@0x1); + timestamp::set_time_has_started_for_testing(&aptos_framework); + + let (mint_ref, _, _, _, test_token) = create_fungible_asset(creator); + let metadata = mint_ref.metadata; + let creator_store = create_test_store(creator, metadata); + let aaron_store = create_test_store(aaron, metadata); + + assert!(supply(test_token) == option::some(0), 1); + // Mint + let fa = mint(&mint_ref, 100); + assert!(supply(test_token) == option::some(100), 2); + // Deposit + deposit(creator_store, fa); + // Withdraw + let fa = withdraw(creator, creator_store, 80); + assert!(supply(test_token) == option::some(100), 3); + deposit(aaron_store, fa); + + // Create a permissioned signer + let aaron_permission_handle = permissioned_signer::create_permissioned_handle(aaron); + let aaron_permission_signer = permissioned_signer::signer_from_permissioned(&aaron_permission_handle); + + // Grant aaron_permission_signer permission to withdraw 10 apt + grant_permission(aaron, &aaron_permission_signer, metadata, 10); + + let fa = withdraw(&aaron_permission_signer, aaron_store, 5); + deposit(aaron_store, fa); + + let fa = withdraw(&aaron_permission_signer, aaron_store, 5); + deposit(aaron_store, fa); + + // aaron signer don't abide to the same limit + let fa = withdraw(aaron, aaron_store, 5); + deposit(aaron_store, fa); + + permissioned_signer::destroy_permissioned_handle(aaron_permission_handle); + } + #[deprecated] #[resource_group_member(group = aptos_framework::object::ObjectGroup)] struct FungibleAssetEvents has key {