From 8642ddff9bceb3f2e5d379efd089e6715bb472ae Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 14 Dec 2022 07:42:05 +0700 Subject: [PATCH] [Aptos Framework][Coin] Switch to an explicit opt-in model for coin transfers --- .../framework/aptos-framework/doc/account.md | 6 +- .../aptos-framework/doc/aptos_account.md | 260 ++++++++++++++++++ .../aptos-framework/sources/account.move | 2 +- .../sources/aptos_account.move | 182 +++++++++++- .../src/aptos_framework_sdk_builder.rs | 94 +++++++ 5 files changed, 529 insertions(+), 15 deletions(-) diff --git a/aptos-move/framework/aptos-framework/doc/account.md b/aptos-move/framework/aptos-framework/doc/account.md index b51fc35cbdc71..9cf42dd9990a0 100644 --- a/aptos-move/framework/aptos-framework/doc/account.md +++ b/aptos-move/framework/aptos-framework/doc/account.md @@ -692,7 +692,7 @@ Scheme identifier for MultiEd25519 signatures used to derive authentication keys -
fun create_signer(addr: address): signer
+
public(friend) fun create_signer(addr: address): signer
 
@@ -701,7 +701,7 @@ Scheme identifier for MultiEd25519 signatures used to derive authentication keys Implementation -
native fun create_signer(addr: address): signer;
+
public(friend) native fun create_signer(addr: address): signer;
 
@@ -1527,7 +1527,7 @@ Capability based functions for efficient use. ### Function `create_signer` -
fun create_signer(addr: address): signer
+
public(friend) fun create_signer(addr: address): signer
 
diff --git a/aptos-move/framework/aptos-framework/doc/aptos_account.md b/aptos-move/framework/aptos-framework/doc/aptos_account.md index eff1a3d3fb7fa..609716ad51845 100644 --- a/aptos-move/framework/aptos-framework/doc/aptos_account.md +++ b/aptos-move/framework/aptos-framework/doc/aptos_account.md @@ -5,11 +5,18 @@ +- [Resource `DirectTransferConfig`](#0x1_aptos_account_DirectTransferConfig) +- [Struct `DirectCoinTransferConfigUpdated`](#0x1_aptos_account_DirectCoinTransferConfigUpdated) +- [Struct `DirectTokenTransferConfigUpdated`](#0x1_aptos_account_DirectTokenTransferConfigUpdated) - [Constants](#@Constants_0) - [Function `create_account`](#0x1_aptos_account_create_account) - [Function `transfer`](#0x1_aptos_account_transfer) +- [Function `transfer_coins`](#0x1_aptos_account_transfer_coins) +- [Function `deposit_coins`](#0x1_aptos_account_deposit_coins) - [Function `assert_account_exists`](#0x1_aptos_account_assert_account_exists) - [Function `assert_account_is_registered_for_apt`](#0x1_aptos_account_assert_account_is_registered_for_apt) +- [Function `set_allow_direct_coin_transfers`](#0x1_aptos_account_set_allow_direct_coin_transfers) +- [Function `can_receive_direct_coin_transfers`](#0x1_aptos_account_can_receive_direct_coin_transfers) - [Specification](#@Specification_1) - [Function `create_account`](#@Specification_1_create_account) - [Function `transfer`](#@Specification_1_transfer) @@ -21,15 +28,129 @@ use 0x1::aptos_coin; use 0x1::coin; use 0x1::error; +use 0x1::event; +use 0x1::signer;
+ + +## Resource `DirectTransferConfig` + +Configuration for whether an account can receive direct transfers of coins that they have not registered. + +By default, this is enabled. Users can opt-out by disabling at any time. + + +
struct DirectTransferConfig has key
+
+ + + +
+Fields + + +
+
+allow_arbitrary_coin_transfers: bool +
+
+ +
+
+update_coin_transfer_events: event::EventHandle<aptos_account::DirectCoinTransferConfigUpdated> +
+
+ +
+
+ + +
+ + + +## Struct `DirectCoinTransferConfigUpdated` + +Event emitted when an account's direct coins transfer config is updated. + + +
struct DirectCoinTransferConfigUpdated has drop, store
+
+ + + +
+Fields + + +
+
+new_allow_direct_transfers: bool +
+
+ +
+
+ + +
+ + + +## Struct `DirectTokenTransferConfigUpdated` + +Event emitted when an account's direct NFT token transfer config is updated. + + +
struct DirectTokenTransferConfigUpdated has drop, store
+
+ + + +
+Fields + + +
+
+new_allow_direct_transfers: bool +
+
+ +
+
+ + +
+ ## Constants + + +Account opted out of receiving coins that they did not register to receive. + + +
const EACCOUNT_DOES_NOT_ACCEPT_DIRECT_COIN_TRANSFERS: u64 = 3;
+
+ + + + + +Account opted out of directly receiving NFT tokens. + + +
const EACCOUNT_DOES_NOT_ACCEPT_DIRECT_TOKEN_TRANSFERS: u64 = 4;
+
+ + + Account does not exist. @@ -80,6 +201,8 @@ Basic account creation methods. ## Function `transfer` +Convenient function to transfer APT to a recipient account that might not exist. +This would create the recipient account first, which also registers it to receive APT, before transferring.
public entry fun transfer(source: &signer, to: address, amount: u64)
@@ -101,6 +224,68 @@ Basic account creation methods.
 
 
 
+
+
+
+
+## Function `transfer_coins`
+
+Convenient function to transfer a custom CoinType to a recipient account that might not exist.
+This would create the recipient account first and register it to receive the CoinType, before transferring.
+
+
+
public entry fun transfer_coins<CoinType>(from: &signer, to: address, amount: u64)
+
+ + + +
+Implementation + + +
public entry fun transfer_coins<CoinType>(from: &signer, to: address, amount: u64) acquires DirectTransferConfig {
+    deposit_coins(to, coin::withdraw<CoinType>(from, amount));
+}
+
+ + + +
+ + + +## Function `deposit_coins` + +Convenient function to deposit a custom CoinType into a recipient account that might not exist. +This would create the recipient account first and register it to receive the CoinType, before transferring. + + +
public entry fun deposit_coins<CoinType>(to: address, coins: coin::Coin<CoinType>)
+
+ + + +
+Implementation + + +
public entry fun deposit_coins<CoinType>(to: address, coins: Coin<CoinType>) acquires DirectTransferConfig {
+    if (!account::exists_at(to)) {
+        create_account(to);
+    };
+    if (!coin::is_account_registered<CoinType>(to)) {
+        assert!(
+            can_receive_direct_coin_transfers(to),
+            error::permission_denied(EACCOUNT_DOES_NOT_ACCEPT_DIRECT_COIN_TRANSFERS),
+        );
+        coin::register<CoinType>(&account::create_signer(to));
+    };
+    coin::deposit<CoinType>(to, coins)
+}
+
+ + +
@@ -150,6 +335,81 @@ Basic account creation methods. + + + + +## Function `set_allow_direct_coin_transfers` + +Set whether account can receive direct transfers of coins that they have not explicitly registered to receive. + + +
public entry fun set_allow_direct_coin_transfers(account: &signer, allow: bool)
+
+ + + +
+Implementation + + +
public entry fun set_allow_direct_coin_transfers(account: &signer, allow: bool) acquires DirectTransferConfig {
+    let addr = signer::address_of(account);
+    if (exists<DirectTransferConfig>(addr)) {
+        let direct_transfer_config = borrow_global_mut<DirectTransferConfig>(addr);
+        // Short-circuit to avoid emitting an event if direct transfer config is not changing.
+        if (direct_transfer_config.allow_arbitrary_coin_transfers != allow) {
+            return
+        };
+
+        direct_transfer_config.allow_arbitrary_coin_transfers = allow;
+        emit_event(
+            &mut direct_transfer_config.update_coin_transfer_events,
+            DirectCoinTransferConfigUpdated { new_allow_direct_transfers: allow });
+    } else {
+        let direct_transfer_config = DirectTransferConfig {
+            allow_arbitrary_coin_transfers: allow,
+            update_coin_transfer_events: new_event_handle<DirectCoinTransferConfigUpdated>(account),
+        };
+        emit_event(
+            &mut direct_transfer_config.update_coin_transfer_events,
+            DirectCoinTransferConfigUpdated { new_allow_direct_transfers: allow });
+        move_to(account, direct_transfer_config);
+    };
+}
+
+ + + +
+ + + +## Function `can_receive_direct_coin_transfers` + +Return true if account can receive direct transfers of coins that they have not explicitly registered to +receive. + +By default, this returns true if an account has not explicitly set whether the can receive direct transfers. + + +
public fun can_receive_direct_coin_transfers(account: address): bool
+
+ + + +
+Implementation + + +
public fun can_receive_direct_coin_transfers(account: address): bool acquires DirectTransferConfig {
+    !exists<DirectTransferConfig>(account) ||
+        borrow_global<DirectTransferConfig>(account).allow_arbitrary_coin_transfers
+}
+
+ + +
diff --git a/aptos-move/framework/aptos-framework/sources/account.move b/aptos-move/framework/aptos-framework/sources/account.move index 89631984f031e..91e735bcbabc2 100644 --- a/aptos-move/framework/aptos-framework/sources/account.move +++ b/aptos-move/framework/aptos-framework/sources/account.move @@ -127,7 +127,7 @@ module aptos_framework::account { /// An attempt to create a resource account on an account that has a committed transaction const EACCOUNT_ALREADY_USED: u64 = 16; - native fun create_signer(addr: address): signer; + public(friend) native fun create_signer(addr: address): signer; #[test_only] /// Create signer for testing, independently of an Aptos-style `Account`. diff --git a/aptos-move/framework/aptos-framework/sources/aptos_account.move b/aptos-move/framework/aptos-framework/sources/aptos_account.move index fef7272bf9cf0..8553cb8a827ae 100644 --- a/aptos-move/framework/aptos-framework/sources/aptos_account.move +++ b/aptos-move/framework/aptos-framework/sources/aptos_account.move @@ -4,6 +4,9 @@ module aptos_framework::aptos_account { use aptos_framework::account; use aptos_framework::aptos_coin::AptosCoin; use aptos_framework::coin; + use aptos_framework::event::{EventHandle, emit_event}; + use std::signer; + use aptos_framework::account::new_event_handle; friend aptos_framework::genesis; friend aptos_framework::resource_account; @@ -12,6 +15,28 @@ module aptos_framework::aptos_account { const EACCOUNT_NOT_FOUND: u64 = 1; /// Account is not registered to receive APT. const EACCOUNT_NOT_REGISTERED_FOR_APT: u64 = 2; + /// Account opted out of receiving coins that they did not register to receive. + const EACCOUNT_DOES_NOT_ACCEPT_DIRECT_COIN_TRANSFERS: u64 = 3; + /// Account opted out of directly receiving NFT tokens. + const EACCOUNT_DOES_NOT_ACCEPT_DIRECT_TOKEN_TRANSFERS: u64 = 4; + + /// Configuration for whether an account can receive direct transfers of coins that they have not registered. + /// + /// By default, this is enabled. Users can opt-out by disabling at any time. + struct DirectTransferConfig has key { + allow_arbitrary_coin_transfers: bool, + update_coin_transfer_events: EventHandle, + } + + /// Event emitted when an account's direct coins transfer config is updated. + struct DirectCoinTransferConfigUpdated has drop, store { + new_allow_direct_transfers: bool, + } + + /// Event emitted when an account's direct NFT token transfer config is updated. + struct DirectTokenTransferConfigUpdated has drop, store { + new_allow_direct_transfers: bool, + } /////////////////////////////////////////////////////////////////////////// /// Basic account creation methods. @@ -22,6 +47,8 @@ module aptos_framework::aptos_account { coin::register(&signer); } + /// Convenient function to transfer APT to a recipient account that might not exist. + /// This would create the recipient account first, which also registers it to receive APT, before transferring. public entry fun transfer(source: &signer, to: address, amount: u64) { if (!account::exists_at(to)) { create_account(to) @@ -29,6 +56,28 @@ module aptos_framework::aptos_account { coin::transfer(source, to, amount) } + /// Convenient function to transfer a custom CoinType to a recipient account that might not exist. + /// This would create the recipient account first and register it to receive the CoinType, before transferring. + public entry fun transfer_coins(from: &signer, to: address, amount: u64) acquires DirectTransferConfig { + deposit_coins(to, coin::withdraw(from, amount)); + } + + /// Convenient function to deposit a custom CoinType into a recipient account that might not exist. + /// This would create the recipient account first and register it to receive the CoinType, before transferring. + public entry fun deposit_coins(to: address, coins: Coin) acquires DirectTransferConfig { + if (!account::exists_at(to)) { + create_account(to); + }; + if (!coin::is_account_registered(to)) { + assert!( + can_receive_direct_coin_transfers(to), + error::permission_denied(EACCOUNT_DOES_NOT_ACCEPT_DIRECT_COIN_TRANSFERS), + ); + coin::register(&account::create_signer(to)); + }; + coin::deposit(to, coins) + } + public fun assert_account_exists(addr: address) { assert!(account::exists_at(addr), error::not_found(EACCOUNT_NOT_FOUND)); } @@ -38,26 +87,137 @@ module aptos_framework::aptos_account { assert!(coin::is_account_registered(addr), error::not_found(EACCOUNT_NOT_REGISTERED_FOR_APT)); } - #[test(alice = @0xa11ce, core = @0x1)] - public fun test_transfer(alice: signer, core: signer) { - use std::signer; - use aptos_std::from_bcs; + /// Set whether `account` can receive direct transfers of coins that they have not explicitly registered to receive. + public entry fun set_allow_direct_coin_transfers(account: &signer, allow: bool) acquires DirectTransferConfig { + let addr = signer::address_of(account); + if (exists(addr)) { + let direct_transfer_config = borrow_global_mut(addr); + // Short-circuit to avoid emitting an event if direct transfer config is not changing. + if (direct_transfer_config.allow_arbitrary_coin_transfers != allow) { + return + }; + + direct_transfer_config.allow_arbitrary_coin_transfers = allow; + emit_event( + &mut direct_transfer_config.update_coin_transfer_events, + DirectCoinTransferConfigUpdated { new_allow_direct_transfers: allow }); + } else { + let direct_transfer_config = DirectTransferConfig { + allow_arbitrary_coin_transfers: allow, + update_coin_transfer_events: new_event_handle(account), + }; + emit_event( + &mut direct_transfer_config.update_coin_transfer_events, + DirectCoinTransferConfigUpdated { new_allow_direct_transfers: allow }); + move_to(account, direct_transfer_config); + }; + } + + /// Return true if `account` can receive direct transfers of coins that they have not explicitly registered to + /// receive. + /// + /// By default, this returns true if an account has not explicitly set whether the can receive direct transfers. + public fun can_receive_direct_coin_transfers(account: address): bool acquires DirectTransferConfig { + !exists(account) || + borrow_global(account).allow_arbitrary_coin_transfers + } + #[test_only] + use aptos_std::from_bcs; + #[test_only] + use std::string::utf8; + use aptos_framework::coin::Coin; + #[test_only] + use aptos_framework::account::create_account_for_test; + + #[test_only] + struct FakeCoin {} + + #[test(alice = @0xa11ce, core = @0x1)] + public fun test_transfer(alice: &signer, core: &signer) { let bob = from_bcs::to_address(x"0000000000000000000000000000000000000000000000000000000000000b0b"); let carol = from_bcs::to_address(x"00000000000000000000000000000000000000000000000000000000000ca501"); - let (burn_cap, mint_cap) = aptos_framework::aptos_coin::initialize_for_test(&core); - create_account(signer::address_of(&alice)); - coin::deposit(signer::address_of(&alice), coin::mint(10000, &mint_cap)); - transfer(&alice, bob, 500); + let (burn_cap, mint_cap) = aptos_framework::aptos_coin::initialize_for_test(core); + create_account(signer::address_of(alice)); + coin::deposit(signer::address_of(alice), coin::mint(10000, &mint_cap)); + transfer(alice, bob, 500); assert!(coin::balance(bob) == 500, 0); - transfer(&alice, carol, 500); + transfer(alice, carol, 500); assert!(coin::balance(carol) == 500, 1); - transfer(&alice, carol, 1500); + transfer(alice, carol, 1500); assert!(coin::balance(carol) == 2000, 2); coin::destroy_burn_cap(burn_cap); coin::destroy_mint_cap(mint_cap); - let _bob = bob; + } + + #[test(from = @0x1, to = @0x12)] + public fun test_direct_coin_transfers(from: &signer, to: &signer) acquires DirectTransferConfig { + let (burn_cap, freeze_cap, mint_cap) = coin::initialize( + from, + utf8(b"FC"), + utf8(b"FC"), + 10, + true, + ); + create_account_for_test(signer::address_of(from)); + create_account_for_test(signer::address_of(to)); + deposit_coins(signer::address_of(from), coin::mint(1000, &mint_cap)); + // Recipient account did not explicit register for the coin. + let to_addr = signer::address_of(to); + transfer_coins(from, to_addr, 500); + assert!(coin::balance(to_addr) == 500, 0); + + coin::destroy_burn_cap(burn_cap); + coin::destroy_mint_cap(mint_cap); + coin::destroy_freeze_cap(freeze_cap); + } + + #[test(from = @0x1, to = @0x12)] + public fun test_direct_coin_transfers_with_explicit_direct_coin_transfer_config( + from: &signer, to: &signer) acquires DirectTransferConfig { + let (burn_cap, freeze_cap, mint_cap) = coin::initialize( + from, + utf8(b"FC"), + utf8(b"FC"), + 10, + true, + ); + create_account_for_test(signer::address_of(from)); + create_account_for_test(signer::address_of(to)); + set_allow_direct_coin_transfers(from, true); + deposit_coins(signer::address_of(from), coin::mint(1000, &mint_cap)); + // Recipient account did not explicit register for the coin. + let to_addr = signer::address_of(to); + transfer_coins(from, to_addr, 500); + assert!(coin::balance(to_addr) == 500, 0); + + coin::destroy_burn_cap(burn_cap); + coin::destroy_mint_cap(mint_cap); + coin::destroy_freeze_cap(freeze_cap); + } + + #[test(from = @0x1, to = @0x12)] + #[expected_failure(abort_code = 0x50003, location = Self)] + public fun test_direct_coin_transfers_fail_if_recipient_opted_out( + from: &signer, to: &signer) acquires DirectTransferConfig { + let (burn_cap, freeze_cap, mint_cap) = coin::initialize( + from, + utf8(b"FC"), + utf8(b"FC"), + 10, + true, + ); + create_account_for_test(signer::address_of(from)); + create_account_for_test(signer::address_of(to)); + set_allow_direct_coin_transfers(from, false); + deposit_coins(signer::address_of(from), coin::mint(1000, &mint_cap)); + // This should fail as the to account has explicitly opted out of receiving arbitrary coins. + transfer_coins(from, signer::address_of(to), 500); + + coin::destroy_burn_cap(burn_cap); + coin::destroy_mint_cap(mint_cap); + coin::destroy_freeze_cap(freeze_cap); } } diff --git a/aptos-move/framework/cached-packages/src/aptos_framework_sdk_builder.rs b/aptos-move/framework/cached-packages/src/aptos_framework_sdk_builder.rs index bda94e758d985..21425d0980e4a 100644 --- a/aptos-move/framework/cached-packages/src/aptos_framework_sdk_builder.rs +++ b/aptos-move/framework/cached-packages/src/aptos_framework_sdk_builder.rs @@ -81,11 +81,26 @@ pub enum EntryFunctionCall { auth_key: AccountAddress, }, + /// Set whether `account` can receive direct transfers of coins that they have not explicitly registered to receive. + AptosAccountSetAllowDirectCoinTransfers { + allow: bool, + }, + + /// Convenient function to transfer APT to a recipient account that might not exist. + /// This would create the recipient account first, which also registers it to receive APT, before transferring. AptosAccountTransfer { to: AccountAddress, amount: u64, }, + /// Convenient function to transfer a custom CoinType to a recipient account that might not exist. + /// This would create the recipient account first and register it to receive the CoinType, before transferring. + AptosAccountTransferCoins { + coin_type: TypeTag, + to: AccountAddress, + amount: u64, + }, + /// Only callable in tests and testnets where the core resources account exists. /// Claim the delegated mint capability and destroy the delegated token. AptosCoinClaimMintCapability {}, @@ -508,7 +523,15 @@ impl EntryFunctionCall { cap_update_table, ), AptosAccountCreateAccount { auth_key } => aptos_account_create_account(auth_key), + AptosAccountSetAllowDirectCoinTransfers { allow } => { + aptos_account_set_allow_direct_coin_transfers(allow) + } AptosAccountTransfer { to, amount } => aptos_account_transfer(to, amount), + AptosAccountTransferCoins { + coin_type, + to, + amount, + } => aptos_account_transfer_coins(coin_type, to, amount), AptosCoinClaimMintCapability {} => aptos_coin_claim_mint_capability(), AptosCoinDelegateMintCapability { to } => aptos_coin_delegate_mint_capability(to), AptosCoinMint { dst_addr, amount } => aptos_coin_mint(dst_addr, amount), @@ -874,6 +897,24 @@ pub fn aptos_account_create_account(auth_key: AccountAddress) -> TransactionPayl )) } +/// Set whether `account` can receive direct transfers of coins that they have not explicitly registered to receive. +pub fn aptos_account_set_allow_direct_coin_transfers(allow: bool) -> TransactionPayload { + TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + AccountAddress::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, + ]), + ident_str!("aptos_account").to_owned(), + ), + ident_str!("set_allow_direct_coin_transfers").to_owned(), + vec![], + vec![bcs::to_bytes(&allow).unwrap()], + )) +} + +/// Convenient function to transfer APT to a recipient account that might not exist. +/// This would create the recipient account first, which also registers it to receive APT, before transferring. pub fn aptos_account_transfer(to: AccountAddress, amount: u64) -> TransactionPayload { TransactionPayload::EntryFunction(EntryFunction::new( ModuleId::new( @@ -889,6 +930,27 @@ pub fn aptos_account_transfer(to: AccountAddress, amount: u64) -> TransactionPay )) } +/// Convenient function to transfer a custom CoinType to a recipient account that might not exist. +/// This would create the recipient account first and register it to receive the CoinType, before transferring. +pub fn aptos_account_transfer_coins( + coin_type: TypeTag, + to: AccountAddress, + amount: u64, +) -> TransactionPayload { + TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + AccountAddress::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, + ]), + ident_str!("aptos_account").to_owned(), + ), + ident_str!("transfer_coins").to_owned(), + vec![coin_type], + vec![bcs::to_bytes(&to).unwrap(), bcs::to_bytes(&amount).unwrap()], + )) +} + /// Only callable in tests and testnets where the core resources account exists. /// Claim the delegated mint capability and destroy the delegated token. pub fn aptos_coin_claim_mint_capability() -> TransactionPayload { @@ -2201,6 +2263,18 @@ mod decoder { } } + pub fn aptos_account_set_allow_direct_coin_transfers( + payload: &TransactionPayload, + ) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some(EntryFunctionCall::AptosAccountSetAllowDirectCoinTransfers { + allow: bcs::from_bytes(script.args().get(0)?).ok()?, + }) + } else { + None + } + } + pub fn aptos_account_transfer(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::AptosAccountTransfer { @@ -2212,6 +2286,18 @@ mod decoder { } } + pub fn aptos_account_transfer_coins(payload: &TransactionPayload) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some(EntryFunctionCall::AptosAccountTransferCoins { + coin_type: script.ty_args().get(0)?.clone(), + to: bcs::from_bytes(script.args().get(0)?).ok()?, + amount: bcs::from_bytes(script.args().get(1)?).ok()?, + }) + } else { + None + } + } + pub fn aptos_coin_claim_mint_capability( payload: &TransactionPayload, ) -> Option { @@ -2979,10 +3065,18 @@ static SCRIPT_FUNCTION_DECODER_MAP: once_cell::sync::Lazy