From f8073f6a3386a7ad5a231ea36d75e5693055a0d7 Mon Sep 17 00:00:00 2001 From: runtianz Date: Mon, 9 Sep 2024 10:01:44 -0700 Subject: [PATCH] create permissioned signer example --- .../aptos-framework/doc/create_signer.md | 10 + .../framework/aptos-framework/doc/overview.md | 1 + .../doc/permissioned_signer.md | 1715 +++++++++++++++++ .../sources/create_signer.move | 1 + .../sources/permissioned_signer.move | 637 ++++++ .../tests/permissioned_signer_tests.move | 322 ++++ 6 files changed, 2686 insertions(+) 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/create_signer.md b/aptos-move/framework/aptos-framework/doc/create_signer.md index a66df38b9c601..5c15eb6bec51f 100644 --- a/aptos-move/framework/aptos-framework/doc/create_signer.md +++ b/aptos-move/framework/aptos-framework/doc/create_signer.md @@ -127,6 +127,16 @@ Convert address to singer and return.
pragma opaque;
 aborts_if [abstract] false;
 ensures [abstract] signer::address_of(result) == addr;
+ensures [abstract] result == spec_create_signer(addr);
+
+ + + + + + + +
fun spec_create_signer(addr: address): signer;
 
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..801de443139aa --- /dev/null +++ b/aptos-move/framework/aptos-framework/doc/permissioned_signer.md @@ -0,0 +1,1715 @@ + + + +# Module `0x1::permissioned_signer` + +A _permissioned signer_ consists of a pair of the original signer and a generated +address which is used store information about associated permissions. + +A permissioned signer is a restricted version of a signer. Functions move_to and +address_of behave the same, and can be passed wherever signer is needed. However, +code can internally query for the permissions 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. + + +- [Resource `GrantedPermissionHandles`](#0x1_permissioned_signer_GrantedPermissionHandles) +- [Enum `PermissionedHandle`](#0x1_permissioned_signer_PermissionedHandle) +- [Enum `StorablePermissionedHandle`](#0x1_permissioned_signer_StorablePermissionedHandle) +- [Enum Resource `PermissionStorage`](#0x1_permissioned_signer_PermissionStorage) +- [Enum `StoredPermission`](#0x1_permissioned_signer_StoredPermission) +- [Enum `Permission`](#0x1_permissioned_signer_Permission) +- [Constants](#@Constants_0) +- [Function `create_permissioned_handle`](#0x1_permissioned_signer_create_permissioned_handle) +- [Function `create_storable_permissioned_handle`](#0x1_permissioned_signer_create_storable_permissioned_handle) +- [Function `destroy_permissioned_handle`](#0x1_permissioned_signer_destroy_permissioned_handle) +- [Function `destroy_storable_permissioned_handle`](#0x1_permissioned_signer_destroy_storable_permissioned_handle) +- [Function `destroy_permissions_storage_address`](#0x1_permissioned_signer_destroy_permissions_storage_address) +- [Function `signer_from_permissioned_handle`](#0x1_permissioned_signer_signer_from_permissioned_handle) +- [Function `signer_from_storable_permissioned_handle`](#0x1_permissioned_signer_signer_from_storable_permissioned_handle) +- [Function `revoke_permission_storage_address`](#0x1_permissioned_signer_revoke_permission_storage_address) +- [Function `revoke_all_handles`](#0x1_permissioned_signer_revoke_all_handles) +- [Function `permissions_storage_address`](#0x1_permissioned_signer_permissions_storage_address) +- [Function `assert_master_signer`](#0x1_permissioned_signer_assert_master_signer) +- [Function `authorize`](#0x1_permissioned_signer_authorize) +- [Function `check_permission_exists`](#0x1_permissioned_signer_check_permission_exists) +- [Function `check_permission_capacity_above`](#0x1_permissioned_signer_check_permission_capacity_above) +- [Function `check_permission_consume`](#0x1_permissioned_signer_check_permission_consume) +- [Function `capacity`](#0x1_permissioned_signer_capacity) +- [Function `revoke_permission`](#0x1_permissioned_signer_revoke_permission) +- [Function `extract_permission`](#0x1_permissioned_signer_extract_permission) +- [Function `get_key`](#0x1_permissioned_signer_get_key) +- [Function `address_of`](#0x1_permissioned_signer_address_of) +- [Function `consume_permission`](#0x1_permissioned_signer_consume_permission) +- [Function `store_permission`](#0x1_permissioned_signer_store_permission) +- [Function `is_permissioned_signer`](#0x1_permissioned_signer_is_permissioned_signer) +- [Function `permission_address`](#0x1_permissioned_signer_permission_address) +- [Function `signer_from_permissioned_handle_impl`](#0x1_permissioned_signer_signer_from_permissioned_handle_impl) +- [Specification](#@Specification_1) + - [Function `create_permissioned_handle`](#@Specification_1_create_permissioned_handle) + - [Function `create_storable_permissioned_handle`](#@Specification_1_create_storable_permissioned_handle) + - [Function `destroy_permissioned_handle`](#@Specification_1_destroy_permissioned_handle) + - [Function `destroy_storable_permissioned_handle`](#@Specification_1_destroy_storable_permissioned_handle) + - [Function `revoke_permission_storage_address`](#@Specification_1_revoke_permission_storage_address) + - [Function `authorize`](#@Specification_1_authorize) + - [Function `check_permission_exists`](#@Specification_1_check_permission_exists) + - [Function `check_permission_capacity_above`](#@Specification_1_check_permission_capacity_above) + - [Function `check_permission_consume`](#@Specification_1_check_permission_consume) + - [Function `capacity`](#@Specification_1_capacity) + - [Function `consume_permission`](#@Specification_1_consume_permission) + - [Function `is_permissioned_signer`](#@Specification_1_is_permissioned_signer) + - [Function `permission_address`](#@Specification_1_permission_address) + - [Function `signer_from_permissioned_handle_impl`](#@Specification_1_signer_from_permissioned_handle_impl) + + +
use 0x1::copyable_any;
+use 0x1::create_signer;
+use 0x1::error;
+use 0x1::option;
+use 0x1::signer;
+use 0x1::simple_map;
+use 0x1::timestamp;
+use 0x1::transaction_context;
+use 0x1::vector;
+
+ + + + + +## Resource `GrantedPermissionHandles` + + + +
struct GrantedPermissionHandles has key
+
+ + + +
+Fields + + +
+
+active_handles: vector<address> +
+
+ +
+
+ + +
+ + + +## Enum `PermissionedHandle` + + + +
enum PermissionedHandle
+
+ + + +
+Variants + + +
+V1 + + +
+Fields + + +
+
+master_account_addr: address +
+
+ +
+
+permissions_storage_addr: address +
+
+ +
+
+ + +
+ +
+ +
+ + + +## Enum `StorablePermissionedHandle` + + + +
enum StorablePermissionedHandle has store
+
+ + + +
+Variants + + +
+V1 + + +
+Fields + + +
+
+master_account_addr: address +
+
+ +
+
+permissions_storage_addr: address +
+
+ +
+
+expiration_time: u64 +
+
+ +
+
+ + +
+ +
+ +
+ + + +## Enum Resource `PermissionStorage` + + + +
enum PermissionStorage has key
+
+ + + +
+Variants + + +
+V1 + + +
+Fields + + +
+
+perms: simple_map::SimpleMap<copyable_any::Any, permissioned_signer::StoredPermission> +
+
+ +
+
+ + +
+ +
+ +
+ + + +## Enum `StoredPermission` + + + +
enum StoredPermission has drop, store
+
+ + + +
+Variants + + +
+Unlimited + + +
+Fields + + +
+
+ + +
+ +
+ +
+Capacity + + +
+Fields + + +
+
+0: u256 +
+
+ +
+
+ + +
+ +
+ +
+ + + +## Enum `Permission` + + + +
enum Permission<K>
+
+ + + +
+Variants + + +
+Capacity + + +
+Fields + + +
+
+owner_address: address +
+
+ +
+
+key: K +
+
+ +
+
+capacity: u256 +
+
+ +
+
+ + +
+ +
+ +
+ + + +## Constants + + + + +Cannot authorize a permission. + + +
const ECANNOT_AUTHORIZE: u64 = 2;
+
+ + + + + +signer doesn't have enough capacity to extract permission. + + +
const ECANNOT_EXTRACT_PERMISSION: u64 = 4;
+
+ + + + + +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;
+
+ + + + + +destroying permission handle that has already been revoked or not owned by the +given master signer. + + +
const E_NOT_ACTIVE: u64 = 8;
+
+ + + + + +permission handle has expired. + + +
const E_PERMISSION_EXPIRED: u64 = 5;
+
+ + + + + +storing extracted permission into a different signer. + + +
const E_PERMISSION_MISMATCH: u64 = 6;
+
+ + + + + +permission handle has been revoked by the original signer. + + +
const E_PERMISSION_REVOKED: u64 = 7;
+
+ + + + + + + +
const U256_MAX: u256 = 115792089237316195423570985008687907853269984665640564039457584007913129639935;
+
+ + + + + +## Function `create_permissioned_handle` + +Create an ephermeral permission handle based on the master signer. + +This handle can be used to derive a signer that can be used in the context of +the current transaction. + + +
public fun create_permissioned_handle(master: &signer): permissioned_signer::PermissionedHandle
+
+ + + +
+Implementation + + +
public fun create_permissioned_handle(master: &signer): PermissionedHandle {
+    assert_master_signer(master);
+    let permissions_storage_addr = generate_auid_address();
+    let master_account_addr = signer::address_of(master);
+
+    move_to(
+        &create_signer(permissions_storage_addr),
+        PermissionStorage::V1 { perms: simple_map::new() }
+    );
+
+    PermissionedHandle::V1 { master_account_addr, permissions_storage_addr }
+}
+
+ + + +
+ + + +## Function `create_storable_permissioned_handle` + +Create an storable permission handle based on the master signer. + +This handle can be used to derive a signer that can be stored by a smart contract. +This is as dangerous as key delegation, thus it remains public(friend) for now. + + +
public(friend) fun create_storable_permissioned_handle(master: &signer, expiration_time: u64): permissioned_signer::StorablePermissionedHandle
+
+ + + +
+Implementation + + +
public(friend) fun create_storable_permissioned_handle(
+    master: &signer, expiration_time: u64
+): StorablePermissionedHandle acquires GrantedPermissionHandles {
+    assert_master_signer(master);
+    let permissions_storage_addr = generate_auid_address();
+    let master_account_addr = signer::address_of(master);
+
+    assert!(
+        timestamp::now_seconds() < expiration_time,
+        error::permission_denied(E_PERMISSION_EXPIRED)
+    );
+
+    if (!exists<GrantedPermissionHandles>(master_account_addr)) {
+        move_to<GrantedPermissionHandles>(
+            master, GrantedPermissionHandles { active_handles: vector::empty() }
+        );
+    };
+
+    vector::push_back(
+        &mut borrow_global_mut<GrantedPermissionHandles>(master_account_addr).active_handles,
+        permissions_storage_addr
+    );
+
+    move_to(
+        &create_signer(permissions_storage_addr),
+        PermissionStorage::V1 { perms: simple_map::new() }
+    );
+
+    StorablePermissionedHandle::V1 {
+        master_account_addr,
+        permissions_storage_addr,
+        expiration_time
+    }
+}
+
+ + + +
+ + + +## Function `destroy_permissioned_handle` + +Destroys an ephermeral permission handle. Clean up the permission stored in that handle + + +
public fun destroy_permissioned_handle(p: permissioned_signer::PermissionedHandle)
+
+ + + +
+Implementation + + +
public fun destroy_permissioned_handle(p: PermissionedHandle) acquires PermissionStorage {
+    let PermissionedHandle::V1 { master_account_addr: _, permissions_storage_addr } =
+        p;
+    destroy_permissions_storage_address(permissions_storage_addr);
+}
+
+ + + +
+ + + +## Function `destroy_storable_permissioned_handle` + +Destroys a storable permission handle. Clean up the permission stored in that handle + + +
public(friend) fun destroy_storable_permissioned_handle(p: permissioned_signer::StorablePermissionedHandle)
+
+ + + +
+Implementation + + +
public(friend) fun destroy_storable_permissioned_handle(
+    p: StorablePermissionedHandle
+) acquires PermissionStorage, GrantedPermissionHandles {
+    let StorablePermissionedHandle::V1 {
+        master_account_addr,
+        permissions_storage_addr,
+        expiration_time: _
+    } = p;
+
+    assert!(
+        exists<GrantedPermissionHandles>(master_account_addr),
+        error::permission_denied(E_PERMISSION_REVOKED),
+    );
+    let granted_permissions =
+        borrow_global_mut<GrantedPermissionHandles>(master_account_addr);
+    let (found, idx) = vector::index_of(
+        &granted_permissions.active_handles, &permissions_storage_addr
+    );
+
+    // Removing the address from the active handle list if it's still active.
+    if(found) {
+        vector::swap_remove(&mut granted_permissions.active_handles, idx);
+    };
+
+    destroy_permissions_storage_address(permissions_storage_addr);
+}
+
+ + + +
+ + + +## Function `destroy_permissions_storage_address` + + + +
fun destroy_permissions_storage_address(permissions_storage_addr: address)
+
+ + + +
+Implementation + + +
inline fun destroy_permissions_storage_address(
+    permissions_storage_addr: address
+) acquires PermissionStorage {
+    if (exists<PermissionStorage>(permissions_storage_addr)) {
+        let PermissionStorage::V1 { perms } =
+            move_from<PermissionStorage>(permissions_storage_addr);
+        simple_map::destroy(
+            perms,
+            |_dk| {},
+            |_dv| {}
+        );
+    }
+}
+
+ + + +
+ + + +## Function `signer_from_permissioned_handle` + +Generate the permissioned signer based on the ephermeral permission handle. + +This signer can be used as a regular signer for other smart contracts. However when such +signer interacts with various framework functions, it would subject to permission checks +and would abort if check fails. + + +
public fun signer_from_permissioned_handle(p: &permissioned_signer::PermissionedHandle): signer
+
+ + + +
+Implementation + + +
public fun signer_from_permissioned_handle(p: &PermissionedHandle): signer {
+    signer_from_permissioned_handle_impl(
+        p.master_account_addr, p.permissions_storage_addr
+    )
+}
+
+ + + +
+ + + +## Function `signer_from_storable_permissioned_handle` + +Generate the permissioned signer based on the storable permission handle. + + +
public(friend) fun signer_from_storable_permissioned_handle(p: &permissioned_signer::StorablePermissionedHandle): signer
+
+ + + +
+Implementation + + +
public(friend) fun signer_from_storable_permissioned_handle(
+    p: &StorablePermissionedHandle
+): signer {
+    assert!(
+        timestamp::now_seconds() < p.expiration_time,
+        error::permission_denied(E_PERMISSION_EXPIRED)
+    );
+    assert!(
+        exists<PermissionStorage>(p.permissions_storage_addr),
+        error::permission_denied(E_PERMISSION_REVOKED)
+    );
+    signer_from_permissioned_handle_impl(
+        p.master_account_addr, p.permissions_storage_addr
+    )
+}
+
+ + + +
+ + + +## Function `revoke_permission_storage_address` + +Revoke a specific storable permission handle immediately. This would disallow owner of +the storable permission handle to derive signer from it anymore. + + +
public entry fun revoke_permission_storage_address(s: &signer, permissions_storage_addr: address)
+
+ + + +
+Implementation + + +
public entry fun revoke_permission_storage_address(
+    s: &signer, permissions_storage_addr: address
+) acquires GrantedPermissionHandles, PermissionStorage {
+    assert!(
+        !is_permissioned_signer(s), error::permission_denied(ENOT_MASTER_SIGNER)
+    );
+    let master_account_addr = signer::address_of(s);
+
+    assert!(
+        exists<GrantedPermissionHandles>(master_account_addr),
+        error::permission_denied(E_PERMISSION_REVOKED),
+    );
+    let granted_permissions =
+        borrow_global_mut<GrantedPermissionHandles>(master_account_addr);
+    let (found, idx) = vector::index_of(
+        &granted_permissions.active_handles, &permissions_storage_addr
+    );
+
+    // The address has to be in the activated list in the master account address.
+    assert!(found, error::permission_denied(E_NOT_ACTIVE));
+    vector::swap_remove(&mut granted_permissions.active_handles, idx);
+    destroy_permissions_storage_address(permissions_storage_addr);
+}
+
+ + + +
+ + + +## Function `revoke_all_handles` + +Revoke all storable permission handle of the signer immediately. + + +
public entry fun revoke_all_handles(s: &signer)
+
+ + + +
+Implementation + + +
public entry fun revoke_all_handles(s: &signer) acquires GrantedPermissionHandles, PermissionStorage {
+    assert!(
+        !is_permissioned_signer(s), error::permission_denied(ENOT_MASTER_SIGNER)
+    );
+    let master_account_addr = signer::address_of(s);
+    if (!exists<GrantedPermissionHandles>(master_account_addr)) { return };
+
+    let granted_permissions =
+        borrow_global_mut<GrantedPermissionHandles>(master_account_addr);
+    let delete_list = vector::trim_reverse(
+        &mut granted_permissions.active_handles, 0
+    );
+    vector::destroy(
+        delete_list,
+        |address| {
+            destroy_permissions_storage_address(address);
+        }
+    )
+}
+
+ + + +
+ + + +## Function `permissions_storage_address` + +Return the permission handle address so that it could be used for revokation purpose. + + +
public(friend) fun permissions_storage_address(p: &permissioned_signer::StorablePermissionedHandle): address
+
+ + + +
+Implementation + + +
public(friend) fun permissions_storage_address(
+    p: &StorablePermissionedHandle
+): address {
+    p.permissions_storage_addr
+}
+
+ + + +
+ + + +## Function `assert_master_signer` + +Helper function that would abort if the signer passed in is a permissioned signer. + + +
public fun assert_master_signer(s: &signer)
+
+ + + +
+Implementation + + +
public fun assert_master_signer(s: &signer) {
+    assert!(
+        !is_permissioned_signer(s), error::permission_denied(ENOT_MASTER_SIGNER)
+    );
+}
+
+ + + +
+ + + +## Function `authorize` + +===================================================================================================== +Permission Management + +Authorizes permissioned with the given permission. This requires to have access to the master +signer. + + +
public fun authorize<PermKey: copy, drop, store>(master: &signer, permissioned: &signer, capacity: u256, perm: PermKey)
+
+ + + +
+Implementation + + +
public fun authorize<PermKey: copy + drop + store>(
+    master: &signer,
+    permissioned: &signer,
+    capacity: u256,
+    perm: PermKey
+) acquires PermissionStorage {
+    assert!(
+        is_permissioned_signer(permissioned)
+            && !is_permissioned_signer(master)
+            && signer::address_of(master) == signer::address_of(permissioned),
+        error::permission_denied(ECANNOT_AUTHORIZE)
+    );
+    let permission_signer_addr = permission_address(permissioned);
+    let perms =
+        &mut borrow_global_mut<PermissionStorage>(permission_signer_addr).perms;
+    let key = copyable_any::pack(perm);
+    if (simple_map::contains_key(perms, &key)) {
+        let entry = simple_map::borrow_mut(perms, &key);
+        match (entry) {
+            StoredPermission::Capacity(current_capacity) => {
+                *current_capacity = *current_capacity + capacity;
+            }
+            StoredPermission::Unlimited => (),
+        }
+    } else {
+        simple_map::add(perms, key, StoredPermission::Capacity(capacity));
+    }
+}
+
+ + + +
+ + + +## Function `check_permission_exists` + + + +
public fun check_permission_exists<PermKey: copy, drop, store>(s: &signer, perm: PermKey): bool
+
+ + + +
+Implementation + + +
public fun check_permission_exists<PermKey: copy + drop + store>(
+    s: &signer, perm: PermKey
+): bool acquires PermissionStorage {
+    if (!is_permissioned_signer(s)) {
+        // master signer has all permissions
+        return true
+    };
+    let addr = permission_address(s);
+    if (!exists<PermissionStorage>(addr)) {
+        return false
+    };
+    simple_map::contains_key(
+        &borrow_global<PermissionStorage>(addr).perms,
+        &copyable_any::pack(perm)
+    )
+}
+
+ + + +
+ + + +## Function `check_permission_capacity_above` + + + +
public fun check_permission_capacity_above<PermKey: copy, drop, store>(s: &signer, threshold: u256, perm: PermKey): bool
+
+ + + +
+Implementation + + +
public fun check_permission_capacity_above<PermKey: copy + drop + store>(
+    s: &signer, threshold: u256, perm: PermKey
+): bool acquires PermissionStorage {
+    if (!is_permissioned_signer(s)) {
+        // master signer has all permissions
+        return true
+    };
+    let addr = permission_address(s);
+    if (!exists<PermissionStorage>(addr)) {
+        return false
+    };
+    let key = copyable_any::pack(perm);
+    let storage = &borrow_global<PermissionStorage>(addr).perms;
+    if (!simple_map::contains_key(storage, &key)) {
+        return false
+    };
+    let perm = simple_map::borrow(storage, &key);
+    match (perm) {
+        StoredPermission::Capacity(capacity) => *capacity > threshold,
+        StoredPermission::Unlimited => true
+    }
+}
+
+ + + +
+ + + +## Function `check_permission_consume` + + + +
public fun check_permission_consume<PermKey: copy, drop, store>(s: &signer, threshold: u256, perm: PermKey): bool
+
+ + + +
+Implementation + + +
public fun check_permission_consume<PermKey: copy + drop + store>(
+    s: &signer, threshold: u256, perm: PermKey
+): bool acquires PermissionStorage {
+    if (!is_permissioned_signer(s)) {
+        // master signer has all permissions
+        return true
+    };
+    let addr = permission_address(s);
+    if (!exists<PermissionStorage>(addr)) {
+        return false
+    };
+    let key = copyable_any::pack(perm);
+    let storage = &mut borrow_global_mut<PermissionStorage>(addr).perms;
+    if (!simple_map::contains_key(storage, &key)) {
+        return false
+    };
+    let perm = simple_map::borrow_mut(storage, &key);
+    match (perm) {
+        StoredPermission::Capacity(current_capacity) => {
+            if (*current_capacity >= threshold) {
+                *current_capacity = *current_capacity - threshold;
+                true
+            } else { false }
+        }
+        StoredPermission::Unlimited => true
+    }
+}
+
+ + + +
+ + + +## Function `capacity` + + + +
public fun capacity<PermKey: copy, drop, store>(s: &signer, perm: PermKey): option::Option<u256>
+
+ + + +
+Implementation + + +
public fun capacity<PermKey: copy + drop + store>(
+    s: &signer, perm: PermKey
+): Option<u256> acquires PermissionStorage {
+    if (!is_permissioned_signer(s)) {
+        return option::some(U256_MAX)
+    };
+    let addr = permission_address(s);
+    if (!exists<PermissionStorage>(addr)) {
+        return option::none()
+    };
+    let perm_storage = &borrow_global<PermissionStorage>(addr).perms;
+    let key = copyable_any::pack(perm);
+    if (simple_map::contains_key(perm_storage, &key)) {
+        let perm = simple_map::borrow(perm_storage, &key);
+        option::some(match (perm) {
+            StoredPermission::Capacity(capacity) => *capacity,
+            StoredPermission::Unlimited => U256_MAX,
+        })
+    } else {
+        option::none()
+    }
+}
+
+ + + +
+ + + +## 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 PermissionStorage {
+    if (!is_permissioned_signer(permissioned)) {
+        // Master signer has no permissions associated with it.
+        return
+    };
+    let addr = permission_address(permissioned);
+    if (!exists<PermissionStorage>(addr)) { return };
+    let perm_storage = &mut borrow_global_mut<PermissionStorage>(addr).perms;
+    let key = copyable_any::pack(perm);
+    if (simple_map::contains_key(perm_storage, &key)) {
+        simple_map::remove(
+            &mut borrow_global_mut<PermissionStorage>(addr).perms,
+            &copyable_any::pack(perm)
+        );
+    }
+}
+
+ + + +
+ + + +## Function `extract_permission` + +Another flavor of api to extract and store permissions + + +
public fun extract_permission<PermKey: copy, drop, store>(s: &signer, weight: u256, perm: PermKey): permissioned_signer::Permission<PermKey>
+
+ + + +
+Implementation + + +
public fun extract_permission<PermKey: copy + drop + store>(
+    s: &signer, weight: u256, perm: PermKey
+): Permission<PermKey> acquires PermissionStorage {
+    assert!(
+        check_permission_consume(s, weight, perm),
+        error::permission_denied(ECANNOT_EXTRACT_PERMISSION)
+    );
+    Permission::Capacity {
+        owner_address: signer::address_of(s),
+        key: perm,
+        capacity: weight
+    }
+}
+
+ + + +
+ + + +## Function `get_key` + + + +
public fun get_key<PermKey>(perm: &permissioned_signer::Permission<PermKey>): &PermKey
+
+ + + +
+Implementation + + +
public fun get_key<PermKey>(perm: &Permission<PermKey>): &PermKey {
+    &perm.key
+}
+
+ + + +
+ + + +## Function `address_of` + + + +
public fun address_of<PermKey>(perm: &permissioned_signer::Permission<PermKey>): address
+
+ + + +
+Implementation + + +
public fun address_of<PermKey>(perm: &Permission<PermKey>): address {
+    perm.owner_address
+}
+
+ + + +
+ + + +## Function `consume_permission` + + + +
public fun consume_permission<PermKey: copy, drop, store>(perm: &mut permissioned_signer::Permission<PermKey>, weight: u256, perm_key: PermKey): bool
+
+ + + +
+Implementation + + +
public fun consume_permission<PermKey: copy + drop + store>(
+    perm: &mut Permission<PermKey>, weight: u256, perm_key: PermKey
+): bool {
+    if (perm.key != perm_key) {
+        return false
+    };
+    if (perm.capacity >= weight) {
+        perm.capacity = perm.capacity - weight;
+        return true
+    } else {
+        return false
+    }
+}
+
+ + + +
+ + + +## Function `store_permission` + + + +
public fun store_permission<PermKey: copy, drop, store>(s: &signer, perm: permissioned_signer::Permission<PermKey>)
+
+ + + +
+Implementation + + +
public fun store_permission<PermKey: copy + drop + store>(
+    s: &signer, perm: Permission<PermKey>
+) acquires PermissionStorage {
+    assert!(
+        is_permissioned_signer(s),
+        error::permission_denied(ENOT_PERMISSIONED_SIGNER)
+    );
+    let Permission::Capacity { key, capacity, owner_address } = perm;
+
+    assert!(
+        signer::address_of(s) == owner_address,
+        error::permission_denied(E_PERMISSION_MISMATCH)
+    );
+
+    let permission_signer_addr = permission_address(s);
+
+    if (!exists<PermissionStorage>(permission_signer_addr)) {
+        let signer = create_signer(permission_signer_addr);
+        move_to(&signer, PermissionStorage::V1 { perms: simple_map::new() });
+    };
+    let perms =
+        &mut borrow_global_mut<PermissionStorage>(permission_signer_addr).perms;
+    let key = copyable_any::pack(key);
+    if (simple_map::contains_key(perms, &key)) {
+        let entry = simple_map::borrow_mut(perms, &key);
+        match (entry) {
+            StoredPermission::Capacity(current_capacity) => {
+                *current_capacity = *current_capacity + capacity;
+            }
+            StoredPermission::Unlimited => (),
+        }
+    } else {
+        simple_map::add(perms, key, StoredPermission::Capacity(capacity))
+    }
+}
+
+ + + +
+ + + +## 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. + + +
public fun is_permissioned_signer(s: &signer): bool
+
+ + + +
+Implementation + + +
public native fun is_permissioned_signer(s: &signer): bool;
+
+ + + +
+ + + +## Function `permission_address` + +Return the address used for storing permissions. Aborts if not a permissioned signer. + + +
fun permission_address(permissioned: &signer): address
+
+ + + +
+Implementation + + +
native fun permission_address(permissioned: &signer): address;
+
+ + + +
+ + + +## Function `signer_from_permissioned_handle_impl` + + +invariants: +signer::address_of(master) == signer::address_of(signer_from_permissioned_handle(create_permissioned_handle(master))), + + +
fun signer_from_permissioned_handle_impl(master_account_addr: address, permissions_storage_addr: address): signer
+
+ + + +
+Implementation + + +
native fun signer_from_permissioned_handle_impl(
+    master_account_addr: address, permissions_storage_addr: address
+): signer;
+
+ + + +
+ + + +## Specification + + + +
axiom forall a: GrantedPermissionHandles:
+    (
+        forall i in 0..len(a.active_handles):
+            forall j in 0..len(a.active_handles):
+                i != j ==>
+                    a.active_handles[i] != a.active_handles[j]
+    );
+
+ + + + + + + +
fun spec_is_permissioned_signer(s: signer): bool;
+
+ + + + + +### Function `create_permissioned_handle` + + +
public fun create_permissioned_handle(master: &signer): permissioned_signer::PermissionedHandle
+
+ + + + +
pragma opaque;
+aborts_if [abstract] spec_is_permissioned_signer(master);
+let permissions_storage_addr = transaction_context::spec_generate_unique_address();
+modifies global<PermissionStorage>(permissions_storage_addr);
+let master_account_addr = signer::address_of(master);
+ensures result.master_account_addr == master_account_addr;
+ensures result.permissions_storage_addr == permissions_storage_addr;
+
+ + + + + +### Function `create_storable_permissioned_handle` + + +
public(friend) fun create_storable_permissioned_handle(master: &signer, expiration_time: u64): permissioned_signer::StorablePermissionedHandle
+
+ + + + +
pragma opaque;
+aborts_if [abstract] spec_is_permissioned_signer(master);
+let permissions_storage_addr = transaction_context::spec_generate_unique_address();
+modifies global<PermissionStorage>(permissions_storage_addr);
+let master_account_addr = signer::address_of(master);
+modifies global<GrantedPermissionHandles>(master_account_addr);
+ensures result.master_account_addr == master_account_addr;
+ensures result.permissions_storage_addr == permissions_storage_addr;
+ensures result.expiration_time == expiration_time;
+ensures vector::spec_contains(
+    global<GrantedPermissionHandles>(master_account_addr).active_handles,
+    permissions_storage_addr
+);
+ensures exists<GrantedPermissionHandles>(master_account_addr);
+
+ + + + + +### Function `destroy_permissioned_handle` + + +
public fun destroy_permissioned_handle(p: permissioned_signer::PermissionedHandle)
+
+ + + + +
ensures !exists<PermissionStorage>(p.permissions_storage_addr);
+
+ + + + + +### Function `destroy_storable_permissioned_handle` + + +
public(friend) fun destroy_storable_permissioned_handle(p: permissioned_signer::StorablePermissionedHandle)
+
+ + + + +
ensures !exists<PermissionStorage>(p.permissions_storage_addr);
+let post granted_permissions = global<GrantedPermissionHandles>(
+    p.master_account_addr
+);
+
+ + + + + +### Function `revoke_permission_storage_address` + + +
public entry fun revoke_permission_storage_address(s: &signer, permissions_storage_addr: address)
+
+ + + + + + +### Function `authorize` + + +
public fun authorize<PermKey: copy, drop, store>(master: &signer, permissioned: &signer, capacity: u256, perm: PermKey)
+
+ + + + +
pragma aborts_if_is_partial;
+aborts_if !spec_is_permissioned_signer(permissioned);
+aborts_if spec_is_permissioned_signer(master);
+aborts_if signer::address_of(permissioned) != signer::address_of(master);
+ensures exists<PermissionStorage>(
+    spec_permission_address(permissioned)
+);
+
+ + + + + +### Function `check_permission_exists` + + +
public fun check_permission_exists<PermKey: copy, drop, store>(s: &signer, perm: PermKey): bool
+
+ + + + +
pragma opaque;
+aborts_if false;
+ensures result == spec_check_permission_exists(s, perm);
+
+ + + + + + + +
fun spec_check_permission_exists<PermKey: copy + drop + store>(s: signer, perm: PermKey): bool {
+   use aptos_std::type_info;
+   use std::bcs;
+   let addr = spec_permission_address(s);
+   let key = Any {
+       type_name: type_info::type_name<PermKey>(),
+       data: bcs::serialize(perm)
+   };
+   if (!spec_is_permissioned_signer(s)) { true }
+   else if (!exists<PermissionStorage>(addr)) { false }
+   else {
+       simple_map::spec_contains_key(global<PermissionStorage>(addr).perms, key)
+   }
+}
+
+ + + + + +### Function `check_permission_capacity_above` + + +
public fun check_permission_capacity_above<PermKey: copy, drop, store>(s: &signer, threshold: u256, perm: PermKey): bool
+
+ + + + +
let permissioned_signer_addr = spec_permission_address(s);
+ensures !spec_is_permissioned_signer(s) ==> result == true;
+ensures (
+    spec_is_permissioned_signer(s)
+        && !exists<PermissionStorage>(permissioned_signer_addr)
+) ==> result == false;
+let key = Any {
+    type_name: type_info::type_name<SimpleMap<Any, u256>>(),
+    data: bcs::serialize(perm)
+};
+
+ + + + + +### Function `check_permission_consume` + + +
public fun check_permission_consume<PermKey: copy, drop, store>(s: &signer, threshold: u256, perm: PermKey): bool
+
+ + + + +
let permissioned_signer_addr = spec_permission_address(s);
+ensures !spec_is_permissioned_signer(s) ==> result == true;
+ensures (
+    spec_is_permissioned_signer(s)
+        && !exists<PermissionStorage>(permissioned_signer_addr)
+) ==> result == false;
+
+ + + + + +### Function `capacity` + + +
public fun capacity<PermKey: copy, drop, store>(s: &signer, perm: PermKey): option::Option<u256>
+
+ + + + + + +### Function `consume_permission` + + +
public fun consume_permission<PermKey: copy, drop, store>(perm: &mut permissioned_signer::Permission<PermKey>, weight: u256, perm_key: PermKey): bool
+
+ + + + +
ensures perm.key != perm_key ==> result == false;
+ensures perm.key == perm_key && old(perm.capacity) < weight ==> result == false;
+ensures perm.key == perm_key
+    && perm.capacity >= weight ==>
+    (perm.capacity == old(perm.capacity) - weight
+        && result == true);
+
+ + + + + +### Function `is_permissioned_signer` + + +
public fun is_permissioned_signer(s: &signer): bool
+
+ + + + +
pragma opaque;
+aborts_if [abstract] false;
+ensures [abstract] result == spec_is_permissioned_signer(s);
+
+ + + + + + + +
fun spec_permission_address(s: signer): address;
+
+ + + + + +### Function `permission_address` + + +
fun permission_address(permissioned: &signer): address
+
+ + + + +
pragma opaque;
+aborts_if [abstract]!spec_is_permissioned_signer(permissioned);
+ensures [abstract] result == spec_permission_address(permissioned);
+
+ + + + + + + +
fun spec_signer_from_permissioned_handle_impl(
+   master_account_addr: address, permissions_storage_addr: address
+): signer;
+
+ + + + + +### Function `signer_from_permissioned_handle_impl` + + +
fun signer_from_permissioned_handle_impl(master_account_addr: address, permissions_storage_addr: address): signer
+
+ + + + +
pragma opaque;
+ensures [abstract] result
+    == spec_signer_from_permissioned_handle_impl(
+        master_account_addr, permissions_storage_addr
+    );
+
+ + +[move-book]: https://aptos.dev/move/book/SUMMARY diff --git a/aptos-move/framework/aptos-framework/sources/create_signer.move b/aptos-move/framework/aptos-framework/sources/create_signer.move index 3da0c50c904f0..a92dd88845568 100644 --- a/aptos-move/framework/aptos-framework/sources/create_signer.move +++ b/aptos-move/framework/aptos-framework/sources/create_signer.move @@ -16,6 +16,7 @@ module aptos_framework::create_signer { friend aptos_framework::genesis; friend aptos_framework::multisig_account; friend aptos_framework::object; + friend aptos_framework::permissioned_signer; public(friend) native fun create_signer(addr: address): signer; } 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..5da091eb23111 --- /dev/null +++ b/aptos-move/framework/aptos-framework/sources/permissioned_signer.move @@ -0,0 +1,637 @@ +/// A _permissioned signer_ consists of a pair of the original signer and a generated +/// address which is used store information about associated permissions. +/// +/// A permissioned signer is a restricted version of a signer. Functions `move_to` and +/// `address_of` behave the same, and can be passed wherever signer is needed. However, +/// code can internally query for the permissions 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; + use std::error; + use std::vector; + use std::option::{Option, Self}; + use aptos_std::copyable_any::{Self, Any}; + use aptos_std::simple_map::{Self, SimpleMap}; + use aptos_framework::create_signer::create_signer; + use aptos_framework::transaction_context::generate_auid_address; + use aptos_framework::timestamp; + + #[test_only] + friend aptos_framework::permissioned_signer_tests; + + /// 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; + + /// permission handle has expired. + const E_PERMISSION_EXPIRED: u64 = 5; + + /// storing extracted permission into a different signer. + const E_PERMISSION_MISMATCH: u64 = 6; + + /// permission handle has been revoked by the original signer. + const E_PERMISSION_REVOKED: u64 = 7; + + /// destroying permission handle that has already been revoked or not owned by the + /// given master signer. + const E_NOT_ACTIVE: u64 = 8; + + const U256_MAX: u256 = + 115792089237316195423570985008687907853269984665640564039457584007913129639935; + + struct GrantedPermissionHandles has key { + active_handles: vector
+ } + + enum PermissionedHandle { + V1 { + master_account_addr: address, + permissions_storage_addr: address + } + } + + enum StorablePermissionedHandle has store { + V1 { + master_account_addr: address, + permissions_storage_addr: address, + expiration_time: u64 + } + } + + enum PermissionStorage has key { + V1 { + perms: SimpleMap + } + } + + enum StoredPermission has store, copy, drop { + Unlimited, + Capacity(u256), + } + + enum Permission { + V1 { + owner_address: address, + key: K, + perm: StoredPermission, + } + } + + /// Create an ephermeral permission handle based on the master signer. + /// + /// This handle can be used to derive a signer that can be used in the context of + /// the current transaction. + public fun create_permissioned_handle(master: &signer): PermissionedHandle { + assert_master_signer(master); + let permissions_storage_addr = generate_auid_address(); + let master_account_addr = signer::address_of(master); + + move_to( + &create_signer(permissions_storage_addr), + PermissionStorage::V1 { perms: simple_map::new() } + ); + + PermissionedHandle::V1 { master_account_addr, permissions_storage_addr } + } + + /// Create an storable permission handle based on the master signer. + /// + /// This handle can be used to derive a signer that can be stored by a smart contract. + /// This is as dangerous as key delegation, thus it remains public(friend) for now. + public(friend) fun create_storable_permissioned_handle( + master: &signer, expiration_time: u64 + ): StorablePermissionedHandle acquires GrantedPermissionHandles { + assert_master_signer(master); + let permissions_storage_addr = generate_auid_address(); + let master_account_addr = signer::address_of(master); + + assert!( + timestamp::now_seconds() < expiration_time, + error::permission_denied(E_PERMISSION_EXPIRED) + ); + + if (!exists(master_account_addr)) { + move_to( + master, GrantedPermissionHandles { active_handles: vector::empty() } + ); + }; + + vector::push_back( + &mut borrow_global_mut(master_account_addr).active_handles, + permissions_storage_addr + ); + + move_to( + &create_signer(permissions_storage_addr), + PermissionStorage::V1 { perms: simple_map::new() } + ); + + StorablePermissionedHandle::V1 { + master_account_addr, + permissions_storage_addr, + expiration_time + } + } + + /// Destroys an ephermeral permission handle. Clean up the permission stored in that handle + public fun destroy_permissioned_handle(p: PermissionedHandle) acquires PermissionStorage { + let PermissionedHandle::V1 { master_account_addr: _, permissions_storage_addr } = + p; + destroy_permissions_storage_address(permissions_storage_addr); + } + + /// Destroys a storable permission handle. Clean up the permission stored in that handle + public(friend) fun destroy_storable_permissioned_handle( + p: StorablePermissionedHandle + ) acquires PermissionStorage, GrantedPermissionHandles { + let StorablePermissionedHandle::V1 { + master_account_addr, + permissions_storage_addr, + expiration_time: _ + } = p; + + assert!( + exists(master_account_addr), + error::permission_denied(E_PERMISSION_REVOKED), + ); + let granted_permissions = + borrow_global_mut(master_account_addr); + let (found, idx) = vector::index_of( + &granted_permissions.active_handles, &permissions_storage_addr + ); + + // Removing the address from the active handle list if it's still active. + if(found) { + vector::swap_remove(&mut granted_permissions.active_handles, idx); + }; + + destroy_permissions_storage_address(permissions_storage_addr); + } + + inline fun destroy_permissions_storage_address( + permissions_storage_addr: address + ) acquires PermissionStorage { + if (exists(permissions_storage_addr)) { + let PermissionStorage::V1 { perms } = + move_from(permissions_storage_addr); + simple_map::destroy( + perms, + |_dk| {}, + |_dv| {} + ); + } + } + + /// Generate the permissioned signer based on the ephermeral permission handle. + /// + /// This signer can be used as a regular signer for other smart contracts. However when such + /// signer interacts with various framework functions, it would subject to permission checks + /// and would abort if check fails. + public fun signer_from_permissioned_handle(p: &PermissionedHandle): signer { + signer_from_permissioned_handle_impl( + p.master_account_addr, p.permissions_storage_addr + ) + } + + /// Generate the permissioned signer based on the storable permission handle. + public(friend) fun signer_from_storable_permissioned_handle( + p: &StorablePermissionedHandle + ): signer { + assert!( + timestamp::now_seconds() < p.expiration_time, + error::permission_denied(E_PERMISSION_EXPIRED) + ); + assert!( + exists(p.permissions_storage_addr), + error::permission_denied(E_PERMISSION_REVOKED) + ); + signer_from_permissioned_handle_impl( + p.master_account_addr, p.permissions_storage_addr + ) + } + + /// Revoke a specific storable permission handle immediately. This would disallow owner of + /// the storable permission handle to derive signer from it anymore. + public entry fun revoke_permission_storage_address( + s: &signer, permissions_storage_addr: address + ) acquires GrantedPermissionHandles, PermissionStorage { + assert!( + !is_permissioned_signer(s), error::permission_denied(ENOT_MASTER_SIGNER) + ); + let master_account_addr = signer::address_of(s); + + assert!( + exists(master_account_addr), + error::permission_denied(E_PERMISSION_REVOKED), + ); + let granted_permissions = + borrow_global_mut(master_account_addr); + let (found, idx) = vector::index_of( + &granted_permissions.active_handles, &permissions_storage_addr + ); + + // The address has to be in the activated list in the master account address. + assert!(found, error::permission_denied(E_NOT_ACTIVE)); + vector::swap_remove(&mut granted_permissions.active_handles, idx); + destroy_permissions_storage_address(permissions_storage_addr); + } + + /// Revoke all storable permission handle of the signer immediately. + public entry fun revoke_all_handles(s: &signer) acquires GrantedPermissionHandles, PermissionStorage { + assert!( + !is_permissioned_signer(s), error::permission_denied(ENOT_MASTER_SIGNER) + ); + let master_account_addr = signer::address_of(s); + if (!exists(master_account_addr)) { return }; + + let granted_permissions = + borrow_global_mut(master_account_addr); + let delete_list = vector::trim_reverse( + &mut granted_permissions.active_handles, 0 + ); + vector::destroy( + delete_list, + |address| { + destroy_permissions_storage_address(address); + } + ) + } + + /// Return the permission handle address so that it could be used for revokation purpose. + public(friend) fun permissions_storage_address( + p: &StorablePermissionedHandle + ): address { + p.permissions_storage_addr + } + + /// Helper function that would abort if the signer passed in is a permissioned signer. + public fun assert_master_signer(s: &signer) { + assert!( + !is_permissioned_signer(s), error::permission_denied(ENOT_MASTER_SIGNER) + ); + } + + /// ===================================================================================================== + /// StoredPermission operations + /// + /// check if StoredPermission has at least `threshold` capacity. + fun is_above(perm: &StoredPermission, threshold: u256): bool { + match (perm) { + StoredPermission::Capacity(capacity) => *capacity > threshold, + StoredPermission::Unlimited => true, + } + } + + /// consume `threshold` capacity from StoredPermission + fun consume_capacity(perm: &mut StoredPermission, threshold: u256): bool { + match (perm) { + StoredPermission::Capacity(current_capacity) => { + if (*current_capacity >= threshold) { + *current_capacity = *current_capacity - threshold; + true + } else { false } + } + StoredPermission::Unlimited => true + } + } + + /// increase `threshold` capacity from StoredPermission + fun increase_capacity(perm: &mut StoredPermission, threshold: u256) { + match (perm) { + StoredPermission::Capacity(current_capacity) => { + *current_capacity = *current_capacity + threshold; + } + StoredPermission::Unlimited => (), + } + } + + /// merge the two stored permission + fun merge(lhs: &mut StoredPermission, rhs: StoredPermission) { + match (rhs) { + StoredPermission::Capacity(new_capacity) => { + match (lhs) { + StoredPermission::Capacity(current_capacity) => { + *current_capacity = *current_capacity + new_capacity; + } + StoredPermission::Unlimited => (), + } + } + StoredPermission::Unlimited => *lhs = StoredPermission::Unlimited, + } + } + + /// ===================================================================================================== + /// Permission Management + /// + /// Authorizes `permissioned` with the given permission. This requires to have access to the `master` + /// signer. + + inline fun map_or( + permissioned: &signer, + perm: PermKey, + mutate: |&mut StoredPermission| T, + default: T, + ): T { + let permission_signer_addr = permission_address(permissioned); + assert!( + exists(permission_signer_addr), + error::permission_denied(E_NOT_ACTIVE) + ); + let perms = + &mut borrow_global_mut(permission_signer_addr).perms; + let key = copyable_any::pack(perm); + if (simple_map::contains_key(perms, &key)) { + mutate(simple_map::borrow_mut(perms, &key)) + } else { + default + } + } + + inline fun insert_or( + permissioned: &signer, + perm: PermKey, + mutate: |&mut StoredPermission|, + default: StoredPermission, + ) { + let permission_signer_addr = permission_address(permissioned); + assert!( + exists(permission_signer_addr), + error::permission_denied(E_NOT_ACTIVE) + ); + let perms = + &mut borrow_global_mut(permission_signer_addr).perms; + let key = copyable_any::pack(perm); + if (simple_map::contains_key(perms, &key)) { + mutate(simple_map::borrow_mut(perms, &key)); + } else { + simple_map::add(perms, key, default); + } + } + + public fun authorize( + master: &signer, + permissioned: &signer, + capacity: u256, + perm: PermKey + ) acquires PermissionStorage { + assert!( + is_permissioned_signer(permissioned) + && !is_permissioned_signer(master) + && signer::address_of(master) == signer::address_of(permissioned), + error::permission_denied(ECANNOT_AUTHORIZE) + ); + insert_or( + permissioned, + perm, + |stored_permission| { + increase_capacity(stored_permission, capacity); + }, + StoredPermission::Capacity(capacity), + ) + } + + /// Authorizes `permissioned` with the given unlimited permission. + /// Unlimited permission can be consumed however many times. + public fun authorize_unlimited( + master: &signer, + permissioned: &signer, + perm: PermKey + ) acquires PermissionStorage { + assert!( + is_permissioned_signer(permissioned) + && !is_permissioned_signer(master) + && signer::address_of(master) == signer::address_of(permissioned), + error::permission_denied(ECANNOT_AUTHORIZE) + ); + insert_or( + permissioned, + perm, + |stored_permission| { + *stored_permission = StoredPermission::Unlimited; + }, + StoredPermission::Unlimited, + ) + } + + public fun check_permission_exists( + s: &signer, perm: PermKey + ): bool acquires PermissionStorage { + check_permission_capacity_above(s, 0, perm) + } + + public fun check_permission_capacity_above( + s: &signer, threshold: u256, perm: PermKey + ): bool acquires PermissionStorage { + if (!is_permissioned_signer(s)) { + // master signer has all permissions + return true + }; + map_or( + s, + perm, + |stored_permission| { + is_above(stored_permission, threshold) + }, + false, + ) + } + + public fun check_permission_consume( + s: &signer, threshold: u256, perm: PermKey + ): bool acquires PermissionStorage { + if (!is_permissioned_signer(s)) { + // master signer has all permissions + return true + }; + map_or( + s, + perm, + |stored_permission| { + consume_capacity(stored_permission, threshold) + }, + false, + ) + } + + public fun capacity( + s: &signer, perm: PermKey + ): Option acquires PermissionStorage { + if (!is_permissioned_signer(s)) { + return option::some(U256_MAX) + }; + map_or( + s, + perm, + |stored_permission: &mut StoredPermission| { + option::some(match (stored_permission) { + StoredPermission::Capacity(capacity) => *capacity, + StoredPermission::Unlimited => U256_MAX, + }) + }, + option::none(), + ) + } + + public fun revoke_permission( + permissioned: &signer, perm: PermKey + ) acquires PermissionStorage { + if (!is_permissioned_signer(permissioned)) { + // Master signer has no permissions associated with it. + return + }; + let addr = permission_address(permissioned); + if (!exists(addr)) { return }; + let perm_storage = &mut borrow_global_mut(addr).perms; + let key = copyable_any::pack(perm); + if (simple_map::contains_key(perm_storage, &key)) { + simple_map::remove( + &mut borrow_global_mut(addr).perms, + ©able_any::pack(perm) + ); + } + } + + /// ===================================================================================================== + /// Another flavor of api to extract and store permissions + /// + /// + public fun extract_permission( + s: &signer, weight: u256, perm: PermKey + ): Permission acquires PermissionStorage { + assert!( + check_permission_consume(s, weight, perm), + error::permission_denied(ECANNOT_EXTRACT_PERMISSION) + ); + Permission::V1 { + owner_address: signer::address_of(s), + key: perm, + perm: StoredPermission::Capacity(weight), + } + } + + public fun extract_all_permission( + s: &signer, perm_key: PermKey + ): Permission acquires PermissionStorage { + assert!( + is_permissioned_signer(s), + error::permission_denied(ECANNOT_EXTRACT_PERMISSION) + ); + let addr = permission_address(s); + assert!( + exists(addr), + error::permission_denied(ECANNOT_EXTRACT_PERMISSION) + ); + let key = copyable_any::pack(perm_key); + let storage = &mut borrow_global_mut(addr).perms; + let (_, value) = simple_map::remove(storage, &key); + + Permission::V1 { + owner_address: signer::address_of(s), + key: perm_key, + perm: value, + } + } + + public fun address_of(perm: &Permission): address { + perm.owner_address + } + + public fun consume_permission( + perm: &mut Permission, weight: u256, perm_key: PermKey + ): bool { + if (perm.key != perm_key) { + return false + }; + consume_capacity(&mut perm.perm, weight) + } + + public fun store_permission( + s: &signer, perm: Permission + ) acquires PermissionStorage { + assert!( + is_permissioned_signer(s), + error::permission_denied(ENOT_PERMISSIONED_SIGNER) + ); + let Permission::V1 { key, perm, owner_address } = perm; + + assert!( + signer::address_of(s) == owner_address, + error::permission_denied(E_PERMISSION_MISMATCH) + ); + + insert_or( + s, + key, + |stored_permission| { + merge(stored_permission, perm); + }, + perm, + ) + } + + // ===================================================================================================== + // Native Functions + /// + /// Check whether this is a permissioned signer. + public native fun is_permissioned_signer(s: &signer): bool; + /// Return the address used for storing permissions. Aborts if not a permissioned signer. + native fun permission_address(permissioned: &signer): address; + /// 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. + /// invariants: + /// signer::address_of(master) == signer::address_of(signer_from_permissioned_handle(create_permissioned_handle(master))), + /// + native fun signer_from_permissioned_handle_impl( + master_account_addr: address, permissions_storage_addr: address + ): signer; + + #[test(creator = @0xcafe)] + fun signer_address_roundtrip( + creator: &signer + ) acquires PermissionStorage, GrantedPermissionHandles { + let aptos_framework = create_signer(@0x1); + timestamp::set_time_has_started_for_testing(&aptos_framework); + + let handle = create_permissioned_handle(creator); + let perm_signer = signer_from_permissioned_handle(&handle); + assert!(signer::address_of(&perm_signer) == signer::address_of(creator), 1); + assert!( + permission_address(&perm_signer) + == handle.permissions_storage_addr, + 1 + ); + assert!(exists(handle.permissions_storage_addr), 1); + + destroy_permissioned_handle(handle); + + let handle = create_storable_permissioned_handle(creator, 60); + let perm_signer = signer_from_storable_permissioned_handle(&handle); + assert!(signer::address_of(&perm_signer) == signer::address_of(creator), 1); + assert!( + permission_address(&perm_signer) + == handle.permissions_storage_addr, + 1 + ); + assert!(exists(handle.permissions_storage_addr), 1); + + destroy_storable_permissioned_handle(handle); + } +} 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..ed0fe04e1e837 --- /dev/null +++ b/aptos-move/framework/aptos-framework/tests/permissioned_signer_tests.move @@ -0,0 +1,322 @@ +#[test_only] +module aptos_framework::permissioned_signer_tests { + use aptos_framework::account::create_signer_for_test; + use aptos_framework::permissioned_signer; + use aptos_framework::timestamp; + 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 aptos_framework = create_signer_for_test(@0x1); + timestamp::set_time_has_started_for_testing(&aptos_framework); + + let perm_handle = permissioned_signer::create_permissioned_handle(creator); + let perm_signer = permissioned_signer::signer_from_permissioned_handle(&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_consume( + &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_consume( + &perm_signer, 10, AddressPermission { addr: @0x1 } + ), + 1 + ); + + assert!( + permissioned_signer::check_permission_consume( + &perm_signer, 5, AddressPermission { addr: @0x1 } + ), + 1 + ); + + // Remaining capacity is 0, should be viewed as non-exist. + assert!( + !permissioned_signer::check_permission_exists( + &perm_signer, 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); + } + + #[test(creator = @0xcafe)] + fun test_storable_permission_e2e(creator: &signer) { + let aptos_framework = create_signer_for_test(@0x1); + timestamp::set_time_has_started_for_testing(&aptos_framework); + + let perm_handle = + permissioned_signer::create_storable_permissioned_handle(creator, 60); + let perm_signer = + permissioned_signer::signer_from_storable_permissioned_handle(&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_consume( + &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_consume( + &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_storable_permissioned_handle(perm_handle); + } + + #[test(creator = @0xcafe)] + #[expected_failure( + abort_code = 0x50005, location = aptos_framework::permissioned_signer + )] + fun test_permission_expiration(creator: &signer) { + let aptos_framework = create_signer_for_test(@0x1); + timestamp::set_time_has_started_for_testing(&aptos_framework); + + let perm_handle = + permissioned_signer::create_storable_permissioned_handle(creator, 60); + let _perm_signer = + permissioned_signer::signer_from_storable_permissioned_handle(&perm_handle); + + timestamp::fast_forward_seconds(60); + let _perm_signer = + permissioned_signer::signer_from_storable_permissioned_handle(&perm_handle); + + permissioned_signer::destroy_storable_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 aptos_framework = create_signer_for_test(@0x1); + timestamp::set_time_has_started_for_testing(&aptos_framework); + + let perm_handle = permissioned_signer::create_permissioned_handle(creator); + let perm_signer = permissioned_signer::signer_from_permissioned_handle(&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 aptos_framework = create_signer_for_test(@0x1); + timestamp::set_time_has_started_for_testing(&aptos_framework); + + let perm_handle = permissioned_signer::create_permissioned_handle(creator); + let perm_signer = permissioned_signer::signer_from_permissioned_handle(&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)] + fun test_invalid_capacity(creator: &signer) { + assert!( + permissioned_signer::capacity(creator, OnePermission {}) + == option::some( + 115792089237316195423570985008687907853269984665640564039457584007913129639935 + ), + 1 + ); + } + + // 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 aptos_framework = create_signer_for_test(@0x1); + timestamp::set_time_has_started_for_testing(&aptos_framework); + + let perm_handle = permissioned_signer::create_permissioned_handle(creator); + let perm_signer = permissioned_signer::signer_from_permissioned_handle(&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); + } + + #[test(creator = @0xcafe)] + fun test_permission_revokation_success(creator: &signer) { + let aptos_framework = create_signer_for_test(@0x1); + timestamp::set_time_has_started_for_testing(&aptos_framework); + + let perm_handle = + permissioned_signer::create_storable_permissioned_handle(creator, 60); + let _perm_signer = + permissioned_signer::signer_from_storable_permissioned_handle(&perm_handle); + + permissioned_signer::revoke_permission_storage_address( + creator, permissioned_signer::permissions_storage_address(&perm_handle) + ); + + permissioned_signer::destroy_storable_permissioned_handle(perm_handle); + } + + #[test(creator = @0xcafe)] + #[expected_failure( + abort_code = 0x50007, location = aptos_framework::permissioned_signer + )] + fun test_permission_revokation_and_access(creator: &signer) { + let aptos_framework = create_signer_for_test(@0x1); + timestamp::set_time_has_started_for_testing(&aptos_framework); + + let perm_handle = + permissioned_signer::create_storable_permissioned_handle(creator, 60); + let _perm_signer = + permissioned_signer::signer_from_storable_permissioned_handle(&perm_handle); + + permissioned_signer::revoke_permission_storage_address( + creator, permissioned_signer::permissions_storage_address(&perm_handle) + ); + let _perm_signer = + permissioned_signer::signer_from_storable_permissioned_handle(&perm_handle); + + permissioned_signer::destroy_storable_permissioned_handle(perm_handle); + } + + #[test(creator1 = @0xcafe, creator2 = @0xbafe)] + #[expected_failure( + abort_code = 0x50008, location = aptos_framework::permissioned_signer + )] + fun test_permission_revoke_other(creator1: &signer, creator2: &signer) { + let aptos_framework = create_signer_for_test(@0x1); + timestamp::set_time_has_started_for_testing(&aptos_framework); + + let perm_handle_1 = + permissioned_signer::create_storable_permissioned_handle(creator1, 60); + + let perm_handle_2 = + permissioned_signer::create_storable_permissioned_handle(creator2, 60); + + permissioned_signer::revoke_permission_storage_address( + creator1, permissioned_signer::permissions_storage_address(&perm_handle_2) + ); + + permissioned_signer::destroy_storable_permissioned_handle(perm_handle_1); + permissioned_signer::destroy_storable_permissioned_handle(perm_handle_2); + } +}