From 1b8ce691bd427605b345dae06c79edd5bfd61e49 Mon Sep 17 00:00:00 2001 From: runtianz Date: Mon, 9 Sep 2024 10:01:44 -0700 Subject: [PATCH] create permissioned signer example --- .../framework/aptos-framework/doc/overview.md | 1 + .../doc/permissioned_signer.md | 981 ++++++++++++++++++ .../sources/permissioned_signer.move | 334 ++++++ .../tests/permissioned_signer_tests.move | 167 +++ 4 files changed, 1483 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/overview.md b/aptos-move/framework/aptos-framework/doc/overview.md index 314baa3612ba96..7ad32f3ea8ec0d 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 00000000000000..8c5a2763ccc3b0 --- /dev/null +++ b/aptos-move/framework/aptos-framework/doc/permissioned_signer.md @@ -0,0 +1,981 @@ + + + +# 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. + + +- [Resource `GrantedPermissionHandles`](#0x1_permissioned_signer_GrantedPermissionHandles) +- [Struct `PermissionedHandle`](#0x1_permissioned_signer_PermissionedHandle) +- [Struct `StorablePermissionedHandle`](#0x1_permissioned_signer_StorablePermissionedHandle) +- [Resource `PermStorage`](#0x1_permissioned_signer_PermStorage) +- [Struct `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 `signer_from_permissioned`](#0x1_permissioned_signer_signer_from_permissioned) +- [Function `signer_from_storable_permissioned`](#0x1_permissioned_signer_signer_from_storable_permissioned) +- [Function `revoke_permission_handle`](#0x1_permissioned_signer_revoke_permission_handle) +- [Function `permission_address`](#0x1_permissioned_signer_permission_address) +- [Function `assert_master_signer`](#0x1_permissioned_signer_assert_master_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 `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_signer`](#0x1_permissioned_signer_permission_signer) +- [Function `signer_from_permissioned_impl`](#0x1_permissioned_signer_signer_from_permissioned_impl) + + +
use 0x1::copyable_any;
+use 0x1::error;
+use 0x1::option;
+use 0x1::signer;
+use 0x1::smart_table;
+use 0x1::timestamp;
+use 0x1::transaction_context;
+use 0x1::vector;
+
+ + + + + +## Resource `GrantedPermissionHandles` + + + +
struct GrantedPermissionHandles has key
+
+ + + +
+Fields + + +
+
+active_handles: vector<address> +
+
+ +
+
+revoked_handles: vector<address> +
+
+ +
+
+ + +
+ + + +## Struct `PermissionedHandle` + + + +
struct PermissionedHandle
+
+ + + +
+Fields + + +
+
+master_addr: address +
+
+ +
+
+permission_addr: address +
+
+ +
+
+ + +
+ + + +## Struct `StorablePermissionedHandle` + + + +
struct StorablePermissionedHandle has store
+
+ + + +
+Fields + + +
+
+master_addr: address +
+
+ +
+
+permission_addr: address +
+
+ +
+
+expiration_time: u64 +
+
+ +
+
+ + +
+ + + +## Resource `PermStorage` + + + +
struct PermStorage has key
+
+ + + +
+Fields + + +
+
+perms: smart_table::SmartTable<copyable_any::Any, u256> +
+
+ +
+
+ + +
+ + + +## Struct `Permission` + + + +
struct Permission<K>
+
+ + + +
+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;
+
+ + + + + +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;
+
+ + + + + +## Function `create_permissioned_handle` + + + +
public fun create_permissioned_handle(master: &signer): permissioned_signer::PermissionedHandle
+
+ + + +
+Implementation + + +
public fun create_permissioned_handle(master: &signer): PermissionedHandle {
+    let permission_addr = generate_auid_address();
+    let master_addr = signer::address_of(master);
+
+    PermissionedHandle {
+        master_addr,
+        permission_addr,
+    }
+}
+
+ + + +
+ + + +## Function `create_storable_permissioned_handle` + + + +
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 {
+    let permission_addr = generate_auid_address();
+    let master_addr = signer::address_of(master);
+
+    if(!exists<GrantedPermissionHandles>(master_addr)) {
+        move_to<GrantedPermissionHandles>(master, GrantedPermissionHandles {
+            active_handles: vector::empty(),
+            revoked_handles: vector::empty(),
+        });
+    };
+
+    vector::push_back(
+        &mut borrow_global_mut<GrantedPermissionHandles>(master_addr).active_handles,
+        permission_addr
+    );
+
+    // Do we need to move sth similar to ObjectCore to register this address as permission address?
+    StorablePermissionedHandle {
+        master_addr,
+        permission_addr,
+        expiration_time,
+    }
+}
+
+ + + +
+ + + +## Function `destroy_permissioned_handle` + + + +
public fun destroy_permissioned_handle(p: permissioned_signer::PermissionedHandle)
+
+ + + +
+Implementation + + +
public fun destroy_permissioned_handle(p: PermissionedHandle) acquires PermStorage {
+    let PermissionedHandle { master_addr: _, permission_addr } = p;
+    if(exists<PermStorage>(permission_addr)) {
+        let PermStorage { perms } = move_from<PermStorage>(permission_addr);
+        smart_table::destroy(perms);
+    };
+}
+
+ + + +
+ + + +## Function `destroy_storable_permissioned_handle` + + + +
public fun destroy_storable_permissioned_handle(p: permissioned_signer::StorablePermissionedHandle)
+
+ + + +
+Implementation + + +
public fun destroy_storable_permissioned_handle(p: StorablePermissionedHandle) acquires PermStorage, GrantedPermissionHandles {
+    let StorablePermissionedHandle { master_addr, permission_addr, expiration_time: _ } = p;
+    if(exists<PermStorage>(permission_addr)) {
+        let PermStorage { perms } = move_from<PermStorage>(permission_addr);
+        smart_table::destroy(perms);
+    };
+    let granted_permissions = borrow_global_mut<GrantedPermissionHandles>(master_addr);
+    let (found, idx) = vector::index_of(&granted_permissions.active_handles, &permission_addr);
+    if(found) {
+        vector::swap_remove(&mut granted_permissions.active_handles, idx);
+    };
+    let (found, idx) = vector::index_of(&granted_permissions.revoked_handles, &permission_addr);
+    if(found) {
+        vector::swap_remove(&mut granted_permissions.revoked_handles, idx);
+    };
+}
+
+ + + +
+ + + +## Function `signer_from_permissioned` + + + +
public fun signer_from_permissioned(p: &permissioned_signer::PermissionedHandle): signer
+
+ + + +
+Implementation + + +
public fun signer_from_permissioned(p: &PermissionedHandle): signer {
+    signer_from_permissioned_impl(p.master_addr, p.permission_addr)
+}
+
+ + + +
+ + + +## Function `signer_from_storable_permissioned` + + + +
public fun signer_from_storable_permissioned(p: &permissioned_signer::StorablePermissionedHandle): signer
+
+ + + +
+Implementation + + +
public fun signer_from_storable_permissioned(p: &StorablePermissionedHandle): signer acquires GrantedPermissionHandles {
+    assert!(timestamp::now_seconds() < p.expiration_time, error::permission_denied(E_PERMISSION_EXPIRED));
+    assert!(
+        !vector::contains(
+            &borrow_global<GrantedPermissionHandles>(p.master_addr).revoked_handles,
+            &p.permission_addr
+        ),
+        error::permission_denied(E_PERMISSION_REVOKED)
+    );
+    signer_from_permissioned_impl(p.master_addr, p.permission_addr)
+}
+
+ + + +
+ + + +## Function `revoke_permission_handle` + + + +
public fun revoke_permission_handle(s: &signer, permission_addr: address)
+
+ + + +
+Implementation + + +
public fun revoke_permission_handle(s: &signer, permission_addr: address) acquires GrantedPermissionHandles {
+    assert!(!is_permissioned_signer(s), error::permission_denied(ENOT_MASTER_SIGNER));
+    let granted_permissions = borrow_global_mut<GrantedPermissionHandles>(signer::address_of(s));
+    if(!vector::contains(&granted_permissions.revoked_handles, &permission_addr)) {
+        vector::push_back(&mut granted_permissions.revoked_handles, permission_addr)
+    }
+}
+
+ + + +
+ + + +## Function `permission_address` + + + +
public fun permission_address(p: &permissioned_signer::PermissionedHandle): address
+
+ + + +
+Implementation + + +
public fun permission_address(p: &PermissionedHandle): address {
+    p.permission_addr
+}
+
+ + + +
+ + + +## Function `assert_master_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 PermStorage {
+    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 = permission_signer(permissioned);
+    let permission_signer_addr = signer::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;
+    let key = copyable_any::pack(perm);
+    if(smart_table::contains(perms, key)) {
+        let entry = smart_table::borrow_mut(perms, key);
+        *entry = *entry + capacity;
+    } else {
+        smart_table::add(perms, key, 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 = signer::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): option::Option<u256>
+
+ + + +
+Implementation + + +
public fun capacity<PermKey: copy + drop + store>(s: &signer, perm: PermKey): Option<u256> acquires PermStorage {
+    assert!(is_permissioned_signer(s), error::permission_denied(ENOT_PERMISSIONED_SIGNER));
+    let addr = signer::address_of(&permission_signer(s));
+    if(!exists<PermStorage>(addr)) {
+        return option::none()
+    };
+    let perm_storage = &borrow_global<PermStorage>(addr).perms;
+    let key = copyable_any::pack(perm);
+    if(smart_table::contains(perm_storage, key)) {
+        option::some(*smart_table::borrow(&borrow_global<PermStorage>(addr).perms, key))
+    } 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 PermStorage {
+    if(!is_permissioned_signer(permissioned)) {
+        // Master signer has no permissions associated with it.
+        return
+    };
+    let addr = signer::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 `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 PermStorage {
+    assert!(check_permission(s, weight, perm), error::permission_denied(ECANNOT_EXTRACT_PERMISSION));
+    Permission {
+        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 PermStorage {
+    assert!(is_permissioned_signer(s), error::permission_denied(ENOT_PERMISSIONED_SIGNER));
+    let Permission { key, capacity, owner_address } = perm;
+
+    assert!(signer::address_of(s) == owner_address, error::permission_denied(E_PERMISSION_MISMATCH));
+
+    let permission_signer = permission_signer(s);
+    let permission_signer_addr = signer::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;
+    let key = copyable_any::pack(key);
+    if(smart_table::contains(perms, key)) {
+        let entry = smart_table::borrow_mut(perms, key);
+        *entry = *entry + capacity;
+    } else {
+        smart_table::add(perms, key, 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_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_impl` + + +invariants: +signer::address_of(master) == signer::address_of(signer_from_permissioned(create_permissioned_handle(master))), + + +
fun signer_from_permissioned_impl(master_addr: address, permission_addr: address): signer
+
+ + + +
+Implementation + + +
native fun signer_from_permissioned_impl(master_addr: address, permission_addr: address): signer;
+
+ + + +
+ + +[move-book]: https://aptos.dev/move/book/SUMMARY 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 00000000000000..8544cf0b9750da --- /dev/null +++ b/aptos-move/framework/aptos-framework/sources/permissioned_signer.move @@ -0,0 +1,334 @@ +/// 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; + use std::error; + use std::vector; + 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; + 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; + + struct GrantedPermissionHandles has key { + active_handles: vector
, + revoked_handles: vector
, + } + + struct PermissionedHandle { + master_addr: address, + permission_addr: address, + } + + struct StorablePermissionedHandle has store { + master_addr: address, + permission_addr: address, + expiration_time: u64, + } + + struct PermStorage has key { + perms: SmartTable, + } + + struct Permission { + owner_address: address, + key: K, + capacity: u256, + } + + public fun create_permissioned_handle(master: &signer): PermissionedHandle { + assert_master_signer(master); + let permission_addr = generate_auid_address(); + let master_addr = signer::address_of(master); + + PermissionedHandle { + master_addr, + permission_addr, + } + } + + public(friend) fun create_storable_permissioned_handle(master: &signer, expiration_time: u64): StorablePermissionedHandle acquires GrantedPermissionHandles { + assert_master_signer(master); + let permission_addr = generate_auid_address(); + let master_addr = signer::address_of(master); + + if(!exists(master_addr)) { + move_to(master, GrantedPermissionHandles { + active_handles: vector::empty(), + revoked_handles: vector::empty(), + }); + }; + + vector::push_back( + &mut borrow_global_mut(master_addr).active_handles, + permission_addr + ); + + // Do we need to move sth similar to ObjectCore to register this address as permission address? + StorablePermissionedHandle { + master_addr, + permission_addr, + expiration_time, + } + } + + public fun destroy_permissioned_handle(p: PermissionedHandle) acquires PermStorage { + let PermissionedHandle { master_addr: _, permission_addr } = p; + if(exists(permission_addr)) { + let PermStorage { perms } = move_from(permission_addr); + smart_table::destroy(perms); + }; + } + + public fun destroy_storable_permissioned_handle(p: StorablePermissionedHandle) acquires PermStorage, GrantedPermissionHandles { + let StorablePermissionedHandle { master_addr, permission_addr, expiration_time: _ } = p; + if(exists(permission_addr)) { + let PermStorage { perms } = move_from(permission_addr); + smart_table::destroy(perms); + }; + let granted_permissions = borrow_global_mut(master_addr); + let (found, idx) = vector::index_of(&granted_permissions.active_handles, &permission_addr); + if(found) { + vector::swap_remove(&mut granted_permissions.active_handles, idx); + }; + let (found, idx) = vector::index_of(&granted_permissions.revoked_handles, &permission_addr); + if(found) { + vector::swap_remove(&mut granted_permissions.revoked_handles, idx); + }; + } + + + public fun signer_from_permissioned(p: &PermissionedHandle): signer { + signer_from_permissioned_impl(p.master_addr, p.permission_addr) + } + + public fun signer_from_storable_permissioned(p: &StorablePermissionedHandle): signer acquires GrantedPermissionHandles { + assert!(timestamp::now_seconds() < p.expiration_time, error::permission_denied(E_PERMISSION_EXPIRED)); + assert!( + !vector::contains( + &borrow_global(p.master_addr).revoked_handles, + &p.permission_addr + ), + error::permission_denied(E_PERMISSION_REVOKED) + ); + signer_from_permissioned_impl(p.master_addr, p.permission_addr) + } + + public fun revoke_permission_handle(s: &signer, permission_addr: address) acquires GrantedPermissionHandles { + assert!(!is_permissioned_signer(s), error::permission_denied(ENOT_MASTER_SIGNER)); + let master_addr = signer::address_of(s); + if(!exists(master_addr)) { + return + }; + let granted_permissions = borrow_global_mut(master_addr); + if(!vector::contains(&granted_permissions.revoked_handles, &permission_addr)) { + vector::push_back(&mut granted_permissions.revoked_handles, permission_addr) + } + } + + public fun permission_address(p: &StorablePermissionedHandle): address { + p.permission_addr + } + + public fun assert_master_signer(s: &signer) { + assert!(!is_permissioned_signer(s), error::permission_denied(ENOT_MASTER_SIGNER)); + } + + /// ===================================================================================================== + /// 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) && + signer::address_of(master) == signer::address_of(permissioned), + error::permission_denied(ECANNOT_AUTHORIZE) + ); + let permission_signer = permission_signer(permissioned); + let permission_signer_addr = signer::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(perm); + if(smart_table::contains(perms, key)) { + let entry = smart_table::borrow_mut(perms, key); + *entry = *entry + capacity; + } else { + smart_table::add(perms, key, 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 = signer::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 = signer::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 = signer::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 { + owner_address: signer::address_of(s), + key: perm, + capacity: weight, + } + } + + public fun get_key(perm: &Permission): &PermKey { + &perm.key + } + + 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 + }; + if(perm.capacity >= weight) { + perm.capacity = perm.capacity - weight; + return true + } else { + return false + } + } + + public fun store_permission( + s: &signer, + perm: Permission + ) acquires PermStorage { + assert!(is_permissioned_signer(s), error::permission_denied(ENOT_PERMISSIONED_SIGNER)); + let Permission { key, capacity, owner_address } = perm; + + assert!(signer::address_of(s) == owner_address, error::permission_denied(E_PERMISSION_MISMATCH)); + + let permission_signer = permission_signer(s); + let permission_signer_addr = signer::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::contains(perms, key)) { + let entry = smart_table::borrow_mut(perms, key); + *entry = *entry + capacity; + } else { + smart_table::add(perms, key, 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: + /// signer::address_of(master) == signer::address_of(signer_from_permissioned(create_permissioned_handle(master))), + /// + native fun signer_from_permissioned_impl(master_addr: address, permission_addr: address): 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 00000000000000..4fbd3c9c3b23ad --- /dev/null +++ b/aptos-move/framework/aptos-framework/tests/permissioned_signer_tests.move @@ -0,0 +1,167 @@ +#[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(&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); + } + + #[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(&perm_handle); + + timestamp::fast_forward_seconds(60); + let _perm_signer = permissioned_signer::signer_from_storable_permissioned(&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(&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(&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 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(&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(&perm_handle); + + permissioned_signer::revoke_permission_handle(creator, permissioned_signer::permission_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(&perm_handle); + + permissioned_signer::revoke_permission_handle(creator, permissioned_signer::permission_address(&perm_handle)); + let _perm_signer = permissioned_signer::signer_from_storable_permissioned(&perm_handle); + + permissioned_signer::destroy_storable_permissioned_handle(perm_handle); + } +}