From 070a951dd3c68addd2ccb2cfd2655159be6a44ee Mon Sep 17 00:00:00 2001 From: runtianz Date: Thu, 29 Aug 2024 16:06:00 -0700 Subject: [PATCH] create permissioned signer example --- .../aptos-framework/doc/aptos_account.md | 1 + .../doc/dispatchable_fungible_asset.md | 1 + .../aptos-framework/doc/fungible_asset.md | 167 +++++++ .../framework/aptos-framework/doc/overview.md | 1 + .../doc/permissioned_signer.md | 407 ++++++++++++++++++ .../sources/aptos_account.move | 1 + .../sources/dispatchable_fungible_asset.move | 1 + .../sources/fungible_asset.move | 98 ++++- .../sources/permissioned_signer.move | 213 +++++++++ .../tests/permissioned_signer_tests.move | 104 +++++ 10 files changed, 993 insertions(+), 1 deletion(-) create mode 100644 aptos-move/framework/aptos-framework/doc/permissioned_signer.md create mode 100644 aptos-move/framework/aptos-framework/sources/permissioned_signer.move create mode 100644 aptos-move/framework/aptos-framework/tests/permissioned_signer_tests.move diff --git a/aptos-move/framework/aptos-framework/doc/aptos_account.md b/aptos-move/framework/aptos-framework/doc/aptos_account.md index 33cd0ff87e5a1..6b4c97226eece 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/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 f12ee14942b4f..1b35c009125da 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,67 @@ 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(permissioned, amount as u256, master, 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/doc/overview.md b/aptos-move/framework/aptos-framework/doc/overview.md index 314baa3612ba9..7ad32f3ea8ec0 100644 --- a/aptos-move/framework/aptos-framework/doc/overview.md +++ b/aptos-move/framework/aptos-framework/doc/overview.md @@ -46,6 +46,7 @@ This is the reference documentation of the Aptos framework. - [`0x1::object`](object.md#0x1_object) - [`0x1::object_code_deployment`](object_code_deployment.md#0x1_object_code_deployment) - [`0x1::optional_aggregator`](optional_aggregator.md#0x1_optional_aggregator) +- [`0x1::permissioned_signer`](permissioned_signer.md#0x1_permissioned_signer) - [`0x1::primary_fungible_store`](primary_fungible_store.md#0x1_primary_fungible_store) - [`0x1::randomness`](randomness.md#0x1_randomness) - [`0x1::randomness_api_v0_config`](randomness_api_v0_config.md#0x1_randomness_api_v0_config) diff --git a/aptos-move/framework/aptos-framework/doc/permissioned_signer.md b/aptos-move/framework/aptos-framework/doc/permissioned_signer.md new file mode 100644 index 0000000000000..7ce1dc5f35872 --- /dev/null +++ b/aptos-move/framework/aptos-framework/doc/permissioned_signer.md @@ -0,0 +1,407 @@ + + + +# Module `0x1::permissioned_signer` + +A _permissioned signer_ consists of a pair of the original signer and a generated +signer which is used store information about associated permissions. + +A permissioned signer behaves compatible with the original signer as it comes to move_to, address_of, and +existing basic signer functionality. However, the permissions can be queried to assert additional +restrictions on the use of the signer. + +A client which is interested in restricting access granted via a signer can create a permissioned signer +and pass on to other existing code without changes to existing APIs. Core functions in the framework, for +example account functions, can then assert availability of permissions, effectively restricting +existing code in a compatible way. + +After introducing the core functionality, examples are provided for withdraw limit on accounts, and +for blind signing. + + +- [Struct `PermissionedHandle`](#0x1_permissioned_signer_PermissionedHandle) +- [Resource `PermStorage`](#0x1_permissioned_signer_PermStorage) +- [Constants](#@Constants_0) +- [Function `create_permissioned_signer`](#0x1_permissioned_signer_create_permissioned_signer) +- [Function `destroy_permissioned_signer`](#0x1_permissioned_signer_destroy_permissioned_signer) +- [Function `authorize`](#0x1_permissioned_signer_authorize) +- [Function `check_permission`](#0x1_permissioned_signer_check_permission) +- [Function `capacity`](#0x1_permissioned_signer_capacity) +- [Function `revoke_permission`](#0x1_permissioned_signer_revoke_permission) +- [Function `is_permissioned_signer`](#0x1_permissioned_signer_is_permissioned_signer) +- [Function `permission_signer`](#0x1_permissioned_signer_permission_signer) +- [Function `signer_from_permissioned`](#0x1_permissioned_signer_signer_from_permissioned) + + +
use 0x1::copyable_any;
+use 0x1::error;
+use 0x1::signer;
+use 0x1::smart_table;
+use 0x1::transaction_context;
+
+ + + + + +## Struct `PermissionedHandle` + + + +
struct PermissionedHandle has store
+
+ + + +
+Fields + + +
+
+master_addr: address +
+
+ +
+
+permission_addr: address +
+
+ +
+
+ + +
+ + + +## Resource `PermStorage` + +===================================================================================================== +Permission Management + + +
struct PermStorage has key
+
+ + + +
+Fields + + +
+
+perms: smart_table::SmartTable<copyable_any::Any, u256> +
+
+ +
+
+ + +
+ + + +## Constants + + + + +Cannot authorize a permission. + + +
const ECANNOT_AUTHORIZE: u64 = 2;
+
+ + + + + +Trying to grant permission using master signer. + + +
const ENOT_MASTER_SIGNER: u64 = 1;
+
+ + + + + +Access permission information from a master signer. + + +
const ENOT_PERMISSIONED_SIGNER: u64 = 3;
+
+ + + + + +## Function `create_permissioned_signer` + + + +
public fun create_permissioned_signer(master: &signer): permissioned_signer::PermissionedHandle
+
+ + + +
+Implementation + + +
public fun create_permissioned_signer(master: &signer): PermissionedHandle {
+    assert!(!is_permissioned_signer(master), error::permission_denied(ENOT_MASTER_SIGNER));
+    // Do we need to move sth similar to ObjectCore to register this address as permission address?
+    PermissionedHandle {
+        master_addr: address_of(master),
+        permission_addr: generate_auid_address(),
+    }
+}
+
+ + + +
+ + + +## Function `destroy_permissioned_signer` + + + +
public fun destroy_permissioned_signer(p: permissioned_signer::PermissionedHandle)
+
+ + + +
+Implementation + + +
public fun destroy_permissioned_signer(p: PermissionedHandle) acquires PermStorage {
+    let PermissionedHandle { master_addr: _, permission_addr } = p;
+    let PermStorage { perms } = move_from<PermStorage>(permission_addr);
+    smart_table::destroy(perms);
+}
+
+ + + +
+ + + +## Function `authorize` + +Authorizes permissioned with the given permission. This requires to have access to the master +signer. + + +
public fun authorize<PermKey: copy, drop, store>(permissioned: &signer, capacity: u256, master: &signer, perm: PermKey)
+
+ + + +
+Implementation + + +
public fun authorize<PermKey: copy + drop + store>(permissioned: &signer, capacity: u256, master: &signer, perm: PermKey) acquires PermStorage {
+    assert!(
+        is_permissioned_signer(permissioned) &&
+        !is_permissioned_signer(master) &&
+        address_of(master) == address_of(permissioned),
+        error::permission_denied(ECANNOT_AUTHORIZE)
+    );
+    let permission_signer = permission_signer(permissioned);
+    let permission_signer_addr = address_of(&permission_signer);
+    if(!exists<PermStorage>(permission_signer_addr)) {
+        move_to(&permission_signer, PermStorage { perms: smart_table::new()});
+    };
+    let perms = &mut borrow_global_mut<PermStorage>(permission_signer_addr).perms;
+    smart_table::add(perms, copyable_any::pack(perm), capacity);
+}
+
+ + + +
+ + + +## Function `check_permission` + +Asserts that the given signer has permission PermKey, and the capacity +to handle weight, which will be subtracted from capacity. + + +
public fun check_permission<PermKey: copy, drop, store>(s: &signer, weight: u256, perm: PermKey): bool
+
+ + + +
+Implementation + + +
public fun check_permission<PermKey: copy + drop + store>(s: &signer, weight: u256, perm: PermKey): bool acquires PermStorage {
+    if (!is_permissioned_signer(s)) {
+        // master signer has all permissions
+        return true
+    };
+    let addr = address_of(&permission_signer(s));
+    if(!exists<PermStorage>(addr)) {
+        return false
+    };
+    let perm = smart_table::borrow_mut(&mut borrow_global_mut<PermStorage>(addr).perms, copyable_any::pack(perm));
+    if(*perm < weight) {
+        return false
+    };
+    *perm = *perm - weight;
+    return true
+}
+
+ + + +
+ + + +## Function `capacity` + + + +
public fun capacity<PermKey: copy, drop, store>(s: &signer, perm: PermKey): u256
+
+ + + +
+Implementation + + +
public fun capacity<PermKey: copy + drop + store>(s: &signer, perm: PermKey): u256 acquires PermStorage {
+    *smart_table::borrow(&borrow_global<PermStorage>(address_of(&permission_signer(s))).perms, copyable_any::pack(perm))
+}
+
+ + + +
+ + + +## Function `revoke_permission` + + + +
public fun revoke_permission<PermKey: copy, drop, store>(permissioned: &signer, perm: PermKey)
+
+ + + +
+Implementation + + +
public fun revoke_permission<PermKey: copy + drop + store>(permissioned: &signer, perm: PermKey) acquires PermStorage {
+    if(!is_permissioned_signer(permissioned)) {
+        // Master signer has no permissions associated with it.
+        return
+    };
+    let addr = address_of(&permission_signer(permissioned));
+    if(!exists<PermStorage>(addr)) {
+        return
+    };
+    smart_table::remove(&mut borrow_global_mut<PermStorage>(addr).perms, copyable_any::pack(perm));
+}
+
+ + + +
+ + + +## Function `is_permissioned_signer` + +Creates a permissioned signer from an existing universal signer. The function aborts if the +given signer is already a permissioned signer. + +The implementation of this function requires to extend the value representation for signers in the VM. + +Check whether this is a permissioned signer. + + +
fun is_permissioned_signer(s: &signer): bool
+
+ + + +
+Implementation + + +
native fun is_permissioned_signer(s: &signer): bool;
+
+ + + +
+ + + +## Function `permission_signer` + +Return the signer used for storing permissions. Aborts if not a permissioned signer. + + +
fun permission_signer(permissioned: &signer): signer
+
+ + + +
+Implementation + + +
native fun permission_signer(permissioned: &signer): signer;
+
+ + + +
+ + + +## Function `signer_from_permissioned` + + +invariants: +address_of(master) == address_of(signer_from_permissioned(create_permissioned_signer(master))), + + +
public fun signer_from_permissioned(p: &permissioned_signer::PermissionedHandle): signer
+
+ + + +
+Implementation + + +
public native fun signer_from_permissioned(p: &PermissionedHandle): signer;
+
+ + + +
+ + +[move-book]: https://aptos.dev/move/book/SUMMARY diff --git a/aptos-move/framework/aptos-framework/sources/aptos_account.move b/aptos-move/framework/aptos-framework/sources/aptos_account.move index 34addca77cf28..e13a84be4772f 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/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 946d7b05eb415..adde6ff33a1a9 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; 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,9 +791,32 @@ 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) } + /// 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, @@ -1179,6 +1208,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; @@ -1541,6 +1596,47 @@ 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 (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 { diff --git a/aptos-move/framework/aptos-framework/sources/permissioned_signer.move b/aptos-move/framework/aptos-framework/sources/permissioned_signer.move new file mode 100644 index 0000000000000..5d2c219cd3ba3 --- /dev/null +++ b/aptos-move/framework/aptos-framework/sources/permissioned_signer.move @@ -0,0 +1,213 @@ +/// A _permissioned signer_ consists of a pair of the original signer and a generated +/// signer which is used store information about associated permissions. +/// +/// A permissioned signer behaves compatible with the original signer as it comes to `move_to`, `address_of`, and +/// existing basic signer functionality. However, the permissions can be queried to assert additional +/// restrictions on the use of the signer. +/// +/// A client which is interested in restricting access granted via a signer can create a permissioned signer +/// and pass on to other existing code without changes to existing APIs. Core functions in the framework, for +/// example account functions, can then assert availability of permissions, effectively restricting +/// existing code in a compatible way. +/// +/// After introducing the core functionality, examples are provided for withdraw limit on accounts, and +/// for blind signing. +module aptos_framework::permissioned_signer { + use std::signer::address_of; + use std::error; + use std::option::{Option, Self}; + use aptos_std::copyable_any::{Self, Any}; + use aptos_std::smart_table::{Self, SmartTable}; + use aptos_framework::transaction_context::generate_auid_address; + + + /// Trying to grant permission using master signer. + const ENOT_MASTER_SIGNER: u64 = 1; + + /// Cannot authorize a permission. + const ECANNOT_AUTHORIZE: u64 = 2; + + /// Access permission information from a master signer. + const ENOT_PERMISSIONED_SIGNER: u64 = 3; + + /// signer doesn't have enough capacity to extract permission. + const ECANNOT_EXTRACT_PERMISSION: u64 = 4; + + struct PermissionedHandle has store { + master_addr: address, + permission_addr: address, + } + + struct PermStorage has key { + perms: SmartTable, + } + + struct Permission { + key: K, + capacity: u256, + } + + public fun create_permissioned_handle(master: &signer): PermissionedHandle { + assert!(!is_permissioned_signer(master), error::permission_denied(ENOT_MASTER_SIGNER)); + // Do we need to move sth similar to ObjectCore to register this address as permission address? + PermissionedHandle { + master_addr: address_of(master), + permission_addr: generate_auid_address(), + } + } + + public fun destroy_permissioned_handle(p: PermissionedHandle) acquires PermStorage { + let PermissionedHandle { master_addr: _, permission_addr } = p; + let PermStorage { perms } = move_from(permission_addr); + smart_table::destroy(perms); + } + + /// ===================================================================================================== + /// Permission Management + /// + + /// Authorizes `permissioned` with the given permission. This requires to have access to the `master` + /// signer. + public fun authorize( + master: &signer, + permissioned: &signer, + capacity: u256, + perm: PermKey + ) acquires PermStorage { + assert!( + is_permissioned_signer(permissioned) && + !is_permissioned_signer(master) && + address_of(master) == address_of(permissioned), + error::permission_denied(ECANNOT_AUTHORIZE) + ); + let permission_signer = permission_signer(permissioned); + let permission_signer_addr = address_of(&permission_signer); + if(!exists(permission_signer_addr)) { + move_to(&permission_signer, PermStorage { perms: smart_table::new()}); + }; + let perms = &mut borrow_global_mut(permission_signer_addr).perms; + smart_table::add(perms, copyable_any::pack(perm), capacity); + } + /// Asserts that the given signer has permission `PermKey`, and the capacity + /// to handle `weight`, which will be subtracted from capacity. + public fun check_permission( + s: &signer, + weight: u256, + perm: PermKey + ): bool acquires PermStorage { + if (!is_permissioned_signer(s)) { + // master signer has all permissions + return true + }; + let addr = address_of(&permission_signer(s)); + if(!exists(addr)) { + return false + }; + let perm = smart_table::borrow_mut(&mut borrow_global_mut(addr).perms, copyable_any::pack(perm)); + if(*perm < weight) { + return false + }; + *perm = *perm - weight; + return true + } + + public fun capacity(s: &signer, perm: PermKey): Option acquires PermStorage { + assert!(is_permissioned_signer(s), error::permission_denied(ENOT_PERMISSIONED_SIGNER)); + let addr = address_of(&permission_signer(s)); + if(!exists(addr)) { + return option::none() + }; + let perm_storage = &borrow_global(addr).perms; + let key = copyable_any::pack(perm); + if(smart_table::contains(perm_storage, key)) { + option::some(*smart_table::borrow(&borrow_global(addr).perms, key)) + } else { + option::none() + } + } + + public fun revoke_permission(permissioned: &signer, perm: PermKey) acquires PermStorage { + if(!is_permissioned_signer(permissioned)) { + // Master signer has no permissions associated with it. + return + }; + let addr = address_of(&permission_signer(permissioned)); + if(!exists(addr)) { + return + }; + smart_table::remove(&mut borrow_global_mut(addr).perms, copyable_any::pack(perm)); + } + + /// Another flavor of api to extract and store permissions + public fun extract_permission( + s: &signer, + weight: u256, + perm: PermKey + ): Permission acquires PermStorage { + assert!(check_permission(s, weight, perm), error::permission_denied(ECANNOT_EXTRACT_PERMISSION)); + Permission { + key: perm, + capacity: weight, + } + } + + public fun get_key(perm: &Permission): &PermKey { + &perm.key + } + + public fun consume_permission( + perm: &mut Permission, + weight: u256, + perm_key: PermKey + ): bool { + if(perm.key != perm_key) { + return false + }; + if(perm.capacity >= weight) { + perm.capcity = perm.capacity - weight; + return true + } else { + return false + } + } + + public fun store_permission( + s: &signer, + perm: Permission + ) { + assert!(is_permissioned_signer(s), error::permission_denied(ENOT_PERMISSIONED_SIGNER)); + let Permission { key, capacity } = perm; + + let permission_signer = permission_signer(permissioned); + let permission_signer_addr = address_of(&permission_signer); + if(!exists(permission_signer_addr)) { + move_to(&permission_signer, PermStorage { perms: smart_table::new()}); + }; + let perms = &mut borrow_global_mut(permission_signer_addr).perms; + let key = copyable_any::pack(key); + if(smart_table::cotains(perms, key)) { + let entry = smart_table::borrow_mut(perms, copyable_any::pack(perm)); + *entry = *entry + capacity; + } else { + smart_table::add(perms, copyable_any::pack(perm), capacity) + } + } + + // ===================================================================================================== + // Native Functions + + /// Creates a permissioned signer from an existing universal signer. The function aborts if the + /// given signer is already a permissioned signer. + /// + /// The implementation of this function requires to extend the value representation for signers in the VM. + /// + /// Check whether this is a permissioned signer. + public native fun is_permissioned_signer(s: &signer): bool; + /// Return the signer used for storing permissions. Aborts if not a permissioned signer. + native fun permission_signer(permissioned: &signer): signer; + /// + /// invariants: + /// address_of(master) == address_of(signer_from_permissioned(create_permissioned_handle(master))), + /// + public native fun signer_from_permissioned(p: &PermissionedHandle): signer; +} diff --git a/aptos-move/framework/aptos-framework/tests/permissioned_signer_tests.move b/aptos-move/framework/aptos-framework/tests/permissioned_signer_tests.move new file mode 100644 index 0000000000000..f996e7e7f014d --- /dev/null +++ b/aptos-move/framework/aptos-framework/tests/permissioned_signer_tests.move @@ -0,0 +1,104 @@ +#[test_only] +module aptos_framework::permissioned_signer_tests { + use aptos_framework::permissioned_signer; + use std::option; + use std::signer; + + struct OnePermission has copy, drop, store {} + + struct AddressPermission has copy, drop, store { + addr: address + } + + + #[test(creator = @0xcafe)] + fun test_permission_e2e( + creator: &signer, + ) { + let perm_handle = permissioned_signer::create_permissioned_handle(creator); + let perm_signer = permissioned_signer::signer_from_permissioned(&perm_handle); + + assert!(permissioned_signer::is_permissioned_signer(&perm_signer), 1); + assert!(!permissioned_signer::is_permissioned_signer(creator), 1); + assert!(signer::address_of(&perm_signer) == signer::address_of(creator), 1); + + permissioned_signer::authorize(creator, &perm_signer, 100, OnePermission {}); + assert!(permissioned_signer::capacity(&perm_signer, OnePermission {}) == option::some(100), 1); + + assert!(permissioned_signer::check_permission(&perm_signer, 10, OnePermission {}), 1); + assert!(permissioned_signer::capacity(&perm_signer, OnePermission {}) == option::some(90), 1); + + permissioned_signer::authorize(creator, &perm_signer, 5, AddressPermission { addr: @0x1 }); + + assert!(permissioned_signer::capacity(&perm_signer, AddressPermission { addr: @0x1 }) == option::some(5), 1); + assert!(permissioned_signer::capacity(&perm_signer, AddressPermission { addr: @0x2 }) == option::none(), 1); + + // Not enough capacity, check permission should return false + assert!(!permissioned_signer::check_permission(&perm_signer, 10, AddressPermission { addr: @0x1 }), 1); + + permissioned_signer::revoke_permission(&perm_signer, OnePermission {}); + assert!(permissioned_signer::capacity(&perm_signer, OnePermission {}) == option::none(), 1); + + permissioned_signer::destroy_permissioned_handle(perm_handle); + } + + // invalid authorization + // 1. master signer is a permissioned signer + // 2. permissioned signer is a master signer + // 3. permissioned and main signer address mismatch + #[test(creator = @0xcafe)] + #[expected_failure(abort_code = 0x50002, location = aptos_framework::permissioned_signer)] + fun test_auth_1( + creator: &signer, + ) { + let perm_handle = permissioned_signer::create_permissioned_handle(creator); + let perm_signer = permissioned_signer::signer_from_permissioned(&perm_handle); + + permissioned_signer::authorize(&perm_signer, &perm_signer, 100, OnePermission {}); + permissioned_signer::destroy_permissioned_handle(perm_handle); + } + + #[test(creator = @0xcafe)] + #[expected_failure(abort_code = 0x50002, location = aptos_framework::permissioned_signer)] + fun test_auth_2( + creator: &signer, + ) { + permissioned_signer::authorize(creator, creator, 100, OnePermission {}); + } + + #[test(creator = @0xcafe, creator2 = @0xbeef)] + #[expected_failure(abort_code = 0x50002, location = aptos_framework::permissioned_signer)] + fun test_auth_3( + creator: &signer, + creator2: &signer, + ) { + let perm_handle = permissioned_signer::create_permissioned_handle(creator); + let perm_signer = permissioned_signer::signer_from_permissioned(&perm_handle); + + permissioned_signer::authorize(creator2, &perm_signer, 100, OnePermission {}); + permissioned_signer::destroy_permissioned_handle(perm_handle); + } + + // Accessing capacity on a master signer + #[test(creator = @0xcafe)] + #[expected_failure(abort_code = 0x50003, location = aptos_framework::permissioned_signer)] + fun test_invalid_capacity( + creator: &signer, + ) { + permissioned_signer::capacity(creator, OnePermission {}); + } + + // creating permission using a permissioned signer + #[test(creator = @0xcafe)] + #[expected_failure(abort_code = 0x50001, location = aptos_framework::permissioned_signer)] + fun test_invalid_creation( + creator: &signer, + ) { + let perm_handle = permissioned_signer::create_permissioned_handle(creator); + let perm_signer = permissioned_signer::signer_from_permissioned(&perm_handle); + + let perm_handle_2 = permissioned_signer::create_permissioned_handle(&perm_signer); + permissioned_signer::destroy_permissioned_handle(perm_handle); + permissioned_signer::destroy_permissioned_handle(perm_handle_2); + } +}