From c557d1d99c4118a23c3254e50ea7e05744981b60 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 + .../aptos-framework/doc/aptos_coin.md | 3 +- .../framework/aptos-framework/doc/coin.md | 24 +- .../doc/dispatchable_fungible_asset.md | 1 + .../aptos-framework/doc/fungible_asset.md | 311 +++++++++++++++++- .../doc/primary_fungible_store.md | 32 ++ .../sources/aptos_account.move | 23 ++ .../sources/aptos_coin.spec.move | 3 +- .../aptos-framework/sources/coin.move | 15 + .../sources/dispatchable_fungible_asset.move | 1 + .../sources/fungible_asset.move | 159 ++++++++- .../sources/primary_fungible_store.move | 11 + 12 files changed, 576 insertions(+), 8 deletions(-) diff --git a/aptos-move/framework/aptos-framework/doc/aptos_account.md b/aptos-move/framework/aptos-framework/doc/aptos_account.md index 4777da7049787..0684910f2d8c4 100644 --- a/aptos-move/framework/aptos-framework/doc/aptos_account.md +++ b/aptos-move/framework/aptos-framework/doc/aptos_account.md @@ -707,6 +707,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/aptos_coin.md b/aptos-move/framework/aptos-framework/doc/aptos_coin.md index 30f3eae067ed8..3e8e237398f2f 100644 --- a/aptos-move/framework/aptos-framework/doc/aptos_coin.md +++ b/aptos-move/framework/aptos-framework/doc/aptos_coin.md @@ -529,7 +529,8 @@ Claim the delegated mint capability and destroy the delegated token. -
let addr = signer::address_of(aptos_framework);
+
aborts_if permissioned_signer::spec_is_permissioned_signer(aptos_framework);
+let addr = signer::address_of(aptos_framework);
 aborts_if addr != @aptos_framework;
 aborts_if !string::spec_internal_check_utf8(b"Aptos Coin");
 aborts_if !string::spec_internal_check_utf8(b"APT");
diff --git a/aptos-move/framework/aptos-framework/doc/coin.md b/aptos-move/framework/aptos-framework/doc/coin.md
index 273f2a2f3e1f0..c8bccab4e87d7 100644
--- a/aptos-move/framework/aptos-framework/doc/coin.md
+++ b/aptos-move/framework/aptos-framework/doc/coin.md
@@ -145,6 +145,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;
@@ -2097,6 +2098,7 @@ Voluntarily migrate to fungible store for CoinType if not yet.
 
public entry fun migrate_to_fungible_store<CoinType>(
     account: &signer
 ) acquires CoinStore, CoinConversionMap, CoinInfo {
+    permissioned_signer::assert_master_signer(account);
     maybe_convert_to_fungible_store<CoinType>(signer::address_of(account));
 }
 
@@ -2953,6 +2955,7 @@ Same as initialize but supply can be initialized to parallelizable monitor_supply: bool, parallelizable: bool, ): (BurnCapability<CoinType>, FreezeCapability<CoinType>, MintCapability<CoinType>) { + permissioned_signer::assert_master_signer(account); let account_addr = signer::address_of(account); assert!( @@ -3070,6 +3073,7 @@ Returns minted Coin.
public fun register<CoinType>(account: &signer) acquires CoinConversionMap {
+    permissioned_signer::assert_master_signer(account);
     let account_addr = signer::address_of(account);
     // Short-circuit and do nothing if account is already registered for CoinType.
     if (is_account_registered<CoinType>(account_addr)) {
@@ -3173,6 +3177,17 @@ Withdraw specified amount of coin CoinType from the si
         amount
     );
     let withdrawn_coin = if (coin_amount_to_withdraw > 0) {
+        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)),
+                coin_amount_to_withdraw
+            );
+        } else {
+            permissioned_signer::assert_master_signer(account);
+        };
+
         let coin_store = borrow_global_mut<CoinStore<CoinType>>(account_addr);
         assert!(
             !coin_store.frozen,
@@ -4162,7 +4177,8 @@ The creator of CoinType must be @aptos_framework.
 
 
 
-
let account_addr = signer::address_of(account);
+
aborts_if permissioned_signer::spec_is_permissioned_signer(account);
+let account_addr = signer::address_of(account);
 // This enforces high-level requirement 1:
 aborts_if type_info::type_of<CoinType>().account_address != account_addr;
 // This enforces high-level requirement 2:
@@ -4184,7 +4200,8 @@ The creator of CoinType must be @aptos_framework.
 
 
 
-
let addr = signer::address_of(account);
+
aborts_if permissioned_signer::spec_is_permissioned_signer(account);
+let addr = signer::address_of(account);
 aborts_if addr != @aptos_framework;
 aborts_if monitor_supply && !exists<aggregator_factory::AggregatorFactory>(@aptos_framework);
 include InitializeInternalSchema<CoinType> {
@@ -4228,7 +4245,8 @@ Only the creator of CoinType can initialize.
 
 
 
-
include InitializeInternalSchema<CoinType> {
+
aborts_if permissioned_signer::spec_is_permissioned_signer(account);
+include InitializeInternalSchema<CoinType> {
     name: name.bytes,
     symbol: symbol.bytes
 };
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 b3b76f108128c..da71484cc0c00 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 17ee3056667da..9444aaf1af41b 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 +567,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 +1172,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 +2722,110 @@ 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_with_permission` + + + +
public fun withdraw_with_permission<T: key>(perm: &mut permissioned_signer::Permission<fungible_asset::WithdrawPermission>, store: object::Object<T>, amount: u64): fungible_asset::FungibleAsset
+
+ + + +
+Implementation + + +
public fun withdraw_with_permission<T: key>(
+    perm: &mut Permission<WithdrawPermission>,
+    store: Object<T>,
+    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)
+}
+
+ + + +
+ + + +## 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_consume(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_consume(owner, amount as u256, WithdrawPermission {
+        metadata_address,
+    }), error::permission_denied(EWITHDRAW_PERMISSION_DENIED));
+}
+
+ + +
@@ -2704,7 +2849,39 @@ Check the permission for withdraw operation. store: Object<T>, 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, + ) +} +
+ + + + + + + +## Function `withdraw_sanity_check_impl` + + + +
fun withdraw_sanity_check_impl<T: key>(owner_address: address, store: object::Object<T>, abort_on_dispatch: bool)
+
+ + + +
+Implementation + + +
inline fun withdraw_sanity_check_impl<T: key>(
+    owner_address: address,
+    store: Object<T>,
+    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),
@@ -3682,6 +3859,138 @@ 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 `grant_apt_permission` + + + +
public fun grant_apt_permission(master: &signer, permissioned: &signer, amount: u64)
+
+ + + +
+Implementation + + +
public fun grant_apt_permission(
+    master: &signer,
+    permissioned: &signer,
+    amount: u64
+) {
+    permissioned_signer::authorize(
+        master,
+        permissioned,
+        amount as u256,
+        WithdrawPermission { metadata_address: @aptos_fungible_asset }
+    )
+}
+
+ + + +
+ + + +## Function `refill_permission_with_fa` + + + +
public(friend) fun refill_permission_with_fa(permissioned: &signer, fa: &fungible_asset::FungibleAsset)
+
+ + + +
+Implementation + + +
public(friend) fun refill_permission_with_fa(
+    permissioned: &signer,
+    fa: &FungibleAsset
+) {
+    permissioned_signer::increase_limit(
+        permissioned,
+        amount(fa) as u256,
+        WithdrawPermission {
+            metadata_address: object::object_address(&metadata_from_asset(fa)),
+        }
+    )
+}
+
+ + + +
+ + + +## 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/doc/primary_fungible_store.md b/aptos-move/framework/aptos-framework/doc/primary_fungible_store.md index 28b1374e4f850..ad5f5312aa431 100644 --- a/aptos-move/framework/aptos-framework/doc/primary_fungible_store.md +++ b/aptos-move/framework/aptos-framework/doc/primary_fungible_store.md @@ -33,6 +33,7 @@ fungible asset to it. This emits an deposit event. - [Function `is_frozen`](#0x1_primary_fungible_store_is_frozen) - [Function `withdraw`](#0x1_primary_fungible_store_withdraw) - [Function `deposit`](#0x1_primary_fungible_store_deposit) +- [Function `deposit_with_signer`](#0x1_primary_fungible_store_deposit_with_signer) - [Function `force_deposit`](#0x1_primary_fungible_store_force_deposit) - [Function `transfer`](#0x1_primary_fungible_store_transfer) - [Function `transfer_assert_minimum_deposit`](#0x1_primary_fungible_store_transfer_assert_minimum_deposit) @@ -507,6 +508,37 @@ Deposit fungible asset fa to the given account's primary store. +
+ + + +## Function `deposit_with_signer` + +Deposit fungible asset fa to the given account's primary store using signer. + +If owner is a permissioned signer, the signer will be granted with permission to withdraw +the same amount of fund in the future. + + +
public fun deposit_with_signer(owner: &signer, fa: fungible_asset::FungibleAsset)
+
+ + + +
+Implementation + + +
public fun deposit_with_signer(owner: &signer, fa: FungibleAsset) acquires DeriveRefPod {
+    fungible_asset::refill_permission_with_fa(owner, &fa);
+    let metadata = fungible_asset::asset_metadata(&fa);
+    let store = ensure_primary_store_exists(signer::address_of(owner), metadata);
+    dispatchable_fungible_asset::deposit(store, fa);
+}
+
+ + +
diff --git a/aptos-move/framework/aptos-framework/sources/aptos_account.move b/aptos-move/framework/aptos-framework/sources/aptos_account.move index d18abf2125186..14f4a2708c66a 100644 --- a/aptos-move/framework/aptos-framework/sources/aptos_account.move +++ b/aptos-move/framework/aptos-framework/sources/aptos_account.move @@ -247,6 +247,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)); } @@ -315,6 +316,28 @@ module aptos_framework::aptos_account { coin::destroy_mint_cap(mint_cap); } + #[test(alice = @0xa11ce, core = @0x1)] + public fun test_transfer_permission(alice: &signer, core: &signer) { + use aptos_framework::permissioned_signer; + + let bob = from_bcs::to_address(x"0000000000000000000000000000000000000000000000000000000000000b0b"); + let carol = from_bcs::to_address(x"00000000000000000000000000000000000000000000000000000000000ca501"); + + let (burn_cap, mint_cap) = aptos_framework::aptos_coin::initialize_for_test(core); + create_account(signer::address_of(alice)); + coin::deposit(signer::address_of(alice), coin::mint(10000, &mint_cap)); + + let perm_handle = permissioned_signer::create_permissioned_handle(alice); + let alice_perm_signer = permissioned_signer::signer_from_permissioned(&perm_handle); + fungible_asset::grant_apt_permission(alice, &alice_perm_signer, 500); + + transfer(&alice_perm_signer, bob, 500); + + coin::destroy_burn_cap(burn_cap); + coin::destroy_mint_cap(mint_cap); + permissioned_signer::destroy_permissioned_handle(perm_handle); + } + #[test(alice = @0xa11ce, core = @0x1)] public fun test_transfer_to_resource_account(alice: &signer, core: &signer) { let (resource_account, _) = account::create_resource_account(alice, vector[]); diff --git a/aptos-move/framework/aptos-framework/sources/aptos_coin.spec.move b/aptos-move/framework/aptos-framework/sources/aptos_coin.spec.move index 9983eee23265f..c542427ee211d 100644 --- a/aptos-move/framework/aptos-framework/sources/aptos_coin.spec.move +++ b/aptos-move/framework/aptos-framework/sources/aptos_coin.spec.move @@ -36,7 +36,8 @@ spec aptos_framework::aptos_coin { spec initialize(aptos_framework: &signer): (BurnCapability, MintCapability) { use aptos_framework::aggregator_factory; - + use aptos_framework::permissioned_signer; + aborts_if permissioned_signer::spec_is_permissioned_signer(aptos_framework); let addr = signer::address_of(aptos_framework); aborts_if addr != @aptos_framework; aborts_if !string::spec_internal_check_utf8(b"Aptos Coin"); diff --git a/aptos-move/framework/aptos-framework/sources/coin.move b/aptos-move/framework/aptos-framework/sources/coin.move index f1d9a81962785..b846c77f1f519 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}; @@ -611,6 +612,7 @@ module aptos_framework::coin { public entry fun migrate_to_fungible_store( account: &signer ) acquires CoinStore, CoinConversionMap, CoinInfo { + permissioned_signer::assert_master_signer(account); maybe_convert_to_fungible_store(signer::address_of(account)); } @@ -954,6 +956,7 @@ module aptos_framework::coin { monitor_supply: bool, parallelizable: bool, ): (BurnCapability, FreezeCapability, MintCapability) { + permissioned_signer::assert_master_signer(account); let account_addr = signer::address_of(account); assert!( @@ -1011,6 +1014,7 @@ module aptos_framework::coin { } public fun register(account: &signer) acquires CoinConversionMap { + permissioned_signer::assert_master_signer(account); let account_addr = signer::address_of(account); // Short-circuit and do nothing if account is already registered for CoinType. if (is_account_registered(account_addr)) { @@ -1054,6 +1058,17 @@ module aptos_framework::coin { 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 5a70aff95d2c1..37c16214fd879 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 50a77348c630c..91ed0202482b2 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_consume(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_consume(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), @@ -1189,6 +1245,58 @@ 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), + } + ) + } + + public fun grant_apt_permission( + master: &signer, + permissioned: &signer, + amount: u64 + ) { + permissioned_signer::authorize( + master, + permissioned, + amount as u256, + WithdrawPermission { metadata_address: @aptos_fungible_asset } + ) + } + + public(friend) fun refill_permission_with_fa( + permissioned: &signer, + fa: &FungibleAsset + ) { + permissioned_signer::increase_limit( + permissioned, + amount(fa) as u256, + WithdrawPermission { + metadata_address: object::object_address(&metadata_from_asset(fa)), + } + ) + } + + /// 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; @@ -1244,6 +1352,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); @@ -1648,6 +1759,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_handle(&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 { diff --git a/aptos-move/framework/aptos-framework/sources/primary_fungible_store.move b/aptos-move/framework/aptos-framework/sources/primary_fungible_store.move index 9e39b97fa2854..19362bf1cd02b 100644 --- a/aptos-move/framework/aptos-framework/sources/primary_fungible_store.move +++ b/aptos-move/framework/aptos-framework/sources/primary_fungible_store.move @@ -168,6 +168,17 @@ module aptos_framework::primary_fungible_store { dispatchable_fungible_asset::deposit(store, fa); } + /// Deposit fungible asset `fa` to the given account's primary store using signer. + /// + /// If `owner` is a permissioned signer, the signer will be granted with permission to withdraw + /// the same amount of fund in the future. + public fun deposit_with_signer(owner: &signer, fa: FungibleAsset) acquires DeriveRefPod { + fungible_asset::refill_permission_with_fa(owner, &fa); + let metadata = fungible_asset::asset_metadata(&fa); + let store = ensure_primary_store_exists(signer::address_of(owner), metadata); + dispatchable_fungible_asset::deposit(store, fa); + } + /// Deposit fungible asset `fa` to the given account's primary store. public(friend) fun force_deposit(owner: address, fa: FungibleAsset) acquires DeriveRefPod { let metadata = fungible_asset::asset_metadata(&fa);