diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/mod.rs index 31bc9009da0..93d51099d80 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/mod.rs @@ -56,6 +56,12 @@ impl TokenConfigurationV0Getters for TokenConfiguration { } } + fn is_allowed_transfer_to_frozen_balance(&self) -> bool { + match self { + TokenConfiguration::V0(v0) => v0.is_allowed_transfer_to_frozen_balance(), + } + } + /// Returns the maximum supply. fn max_supply(&self) -> Option { match self { @@ -167,6 +173,13 @@ impl TokenConfigurationV0Setters for TokenConfiguration { } } + /// Allow or not a transfer and mint tokens to frozen identity token balances + fn allow_transfer_to_frozen_balance(&mut self, allow: bool) { + match self { + TokenConfiguration::V0(v0) => v0.allow_transfer_to_frozen_balance(allow), + } + } + /// Sets the base supply. fn set_base_supply(&mut self, base_supply: u64) { match self { diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/v0/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/v0/mod.rs index 093d4622ea9..8a1f9d246fa 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/v0/mod.rs @@ -27,6 +27,9 @@ pub trait TokenConfigurationV0Getters { /// Returns if we start as paused. fn start_as_paused(&self) -> bool; + /// Allow to transfer and mint tokens to frozen identity token balances + fn is_allowed_transfer_to_frozen_balance(&self) -> bool; + /// Returns the maximum supply. fn max_supply(&self) -> Option; @@ -78,6 +81,9 @@ pub trait TokenConfigurationV0Setters { /// Sets the conventions change rules. fn set_conventions_change_rules(&mut self, rules: ChangeControlRules); + /// Allow or not a transfer and mint tokens to frozen identity token balances + fn allow_transfer_to_frozen_balance(&mut self, allow: bool); + /// Sets the base supply. fn set_base_supply(&mut self, base_supply: TokenAmount); diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/accessors.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/accessors.rs index 889df4e4d0a..d08afffa688 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/accessors.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/accessors.rs @@ -45,6 +45,11 @@ impl TokenConfigurationV0Getters for TokenConfigurationV0 { self.start_as_paused } + /// Allow to transfer and mint tokens to frozen identity token balances + fn is_allowed_transfer_to_frozen_balance(&self) -> bool { + self.allow_transfer_to_frozen_balance + } + /// Returns the maximum supply. fn max_supply(&self) -> Option { self.max_supply @@ -168,6 +173,11 @@ impl TokenConfigurationV0Setters for TokenConfigurationV0 { self.conventions_change_rules = rules; } + /// Allow or not a transfer and mint tokens to frozen identity token balances + fn allow_transfer_to_frozen_balance(&mut self, allow: bool) { + self.allow_transfer_to_frozen_balance = allow; + } + /// Sets the base supply. fn set_base_supply(&mut self, base_supply: TokenAmount) { self.base_supply = base_supply; diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/mod.rs index fed112ee003..52bd41a2344 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/mod.rs @@ -34,6 +34,9 @@ pub struct TokenConfigurationV0 { /// Do we start off as paused, meaning that we can not transfer till we unpause. #[serde(default = "default_starts_as_paused")] pub start_as_paused: bool, + /// Allow to transfer and mint tokens to frozen identity token balances + #[serde(default = "default_allow_transfer_to_frozen_balance")] + pub allow_transfer_to_frozen_balance: bool, /// Who can change the max supply /// Even if set no one can ever change this under the base supply #[serde(default = "default_change_control_rules")] @@ -71,6 +74,11 @@ fn default_starts_as_paused() -> bool { false } +// Default function for `allow_transfer_to_frozen_balance` +fn default_allow_transfer_to_frozen_balance() -> bool { + true +} + fn default_token_keeps_history_rules() -> TokenKeepsHistoryRules { TokenKeepsHistoryRules::V0(TokenKeepsHistoryRulesV0 { keeps_transfer_history: true, @@ -143,13 +151,14 @@ impl fmt::Display for TokenConfigurationV0 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "TokenConfigurationV0 {{\n conventions: {:?},\n conventions_change_rules: {:?},\n base_supply: {},\n max_supply: {:?},\n keeps_history: {},\n start_as_paused: {},\n max_supply_change_rules: {:?},\n distribution_rules: {},\n manual_minting_rules: {:?},\n manual_burning_rules: {:?},\n freeze_rules: {:?},\n unfreeze_rules: {:?},\n destroy_frozen_funds_rules: {:?},\n emergency_action_rules: {:?},\n main_control_group: {:?},\n main_control_group_can_be_modified: {:?}\n}}", + "TokenConfigurationV0 {{\n conventions: {:?},\n conventions_change_rules: {:?},\n base_supply: {},\n max_supply: {:?},\n keeps_history: {},\n start_as_paused: {},\n allow_transfer_to_frozen_balance: {},\n max_supply_change_rules: {:?},\n distribution_rules: {},\n manual_minting_rules: {:?},\n manual_burning_rules: {:?},\n freeze_rules: {:?},\n unfreeze_rules: {:?},\n destroy_frozen_funds_rules: {:?},\n emergency_action_rules: {:?},\n main_control_group: {:?},\n main_control_group_can_be_modified: {:?}\n}}", self.conventions, self.conventions_change_rules, self.base_supply, self.max_supply, self.keeps_history, self.start_as_paused, + self.allow_transfer_to_frozen_balance, self.max_supply_change_rules, self.distribution_rules, self.manual_minting_rules, @@ -190,6 +199,7 @@ impl TokenConfigurationV0 { keeps_direct_purchase_history: true, }), start_as_paused: false, + allow_transfer_to_frozen_balance: true, max_supply_change_rules: ChangeControlRulesV0 { authorized_to_make_change: AuthorizedActionTakers::NoOne, admin_action_takers: AuthorizedActionTakers::NoOne, diff --git a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/test/tokens.rs b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/test/tokens.rs index c53010b7869..a7ca5d32ffb 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/test/tokens.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/test/tokens.rs @@ -240,6 +240,7 @@ impl Platform { max_supply: None, keeps_history: TokenKeepsHistoryRulesV0::default().into(), start_as_paused: false, + allow_transfer_to_frozen_balance: true, max_supply_change_rules: ChangeControlRulesV0::default().into(), distribution_rules: TokenDistributionRulesV0 { perpetual_distribution: None, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_transition_action/state_v0/mod.rs index d30cb0d377d..56ad9bda51e 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_transition_action/state_v0/mod.rs @@ -2,16 +2,18 @@ use dpp::block::block_info::BlockInfo; use dpp::consensus::ConsensusError; use dpp::consensus::state::identity::RecipientIdentityDoesNotExistError; use dpp::consensus::state::state_error::StateError; -use dpp::consensus::state::token::TokenMintPastMaxSupplyError; +use dpp::consensus::state::token::{IdentityTokenAccountFrozenError, TokenMintPastMaxSupplyError}; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::accessors::v1::DataContractV1Getters; use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; use dpp::prelude::Identifier; +use dpp::tokens::info::v0::IdentityTokenInfoV0Accessors; use dpp::validation::SimpleConsensusValidationResult; use drive::state_transition_action::batch::batched_transition::token_transition::token_mint_transition_action::{TokenMintTransitionAction, TokenMintTransitionActionAccessorsV0}; use dpp::version::PlatformVersion; use drive::error::drive::DriveError; use drive::query::TransactionArg; +use drive::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionActionAccessorsV0; use crate::error::Error; use crate::execution::types::execution_operation::{RetrieveIdentityInfo, ValidationOperation}; use crate::execution::types::state_transition_execution_context::{StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0}; @@ -142,6 +144,39 @@ impl TokenMintTransitionActionStateValidationV0 for TokenMintTransitionAction { } } + // We need to verify that account we are transferring to not frozen + if !self + .base() + .token_configuration()? + .is_allowed_transfer_to_frozen_balance() + { + let (info, fee_result) = platform.drive.fetch_identity_token_info_with_costs( + self.token_id().to_buffer(), + recipient.to_buffer(), + block_info, + true, + transaction, + platform_version, + )?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + if let Some(info) = info { + if info.frozen() { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::IdentityTokenAccountFrozenError( + IdentityTokenAccountFrozenError::new( + self.token_id(), + recipient, + "mint".to_string(), + ), + )), + )); + } + }; + } + Ok(SimpleConsensusValidationResult::new()) } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_transfer_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_transfer_transition_action/state_v0/mod.rs index abca52e3b12..15d0f00c8d7 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_transfer_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_transfer_transition_action/state_v0/mod.rs @@ -2,6 +2,7 @@ use dpp::block::block_info::BlockInfo; use dpp::consensus::ConsensusError; use dpp::consensus::state::state_error::StateError; use dpp::consensus::state::token::{IdentityDoesNotHaveEnoughTokenBalanceError, IdentityTokenAccountFrozenError, TokenIsPausedError, TokenTransferRecipientIdentityNotExistError}; +use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; use dpp::prelude::Identifier; use dpp::tokens::info::v0::IdentityTokenInfoV0Accessors; use dpp::tokens::status::v0::TokenStatusV0Accessors; @@ -9,6 +10,7 @@ use dpp::validation::SimpleConsensusValidationResult; use drive::state_transition_action::batch::batched_transition::token_transition::token_transfer_transition_action::TokenTransferTransitionAction; use dpp::version::PlatformVersion; use drive::query::TransactionArg; +use drive::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionActionAccessorsV0; use drive::state_transition_action::batch::batched_transition::token_transition::token_transfer_transition_action::v0::TokenTransferTransitionActionAccessorsV0; use crate::error::Error; use crate::execution::types::execution_operation::ValidationOperation; @@ -78,6 +80,7 @@ impl TokenTransferTransitionActionStateValidationV0 for TokenTransferTransitionA } // We need to verify that our token account is not frozen + let (info, fee_result) = platform.drive.fetch_identity_token_info_with_costs( self.token_id().to_buffer(), owner_id.to_buffer(), @@ -103,6 +106,39 @@ impl TokenTransferTransitionActionStateValidationV0 for TokenTransferTransitionA } }; + // We need to verify that account we are transferring to not frozen + if !self + .base() + .token_configuration()? + .is_allowed_transfer_to_frozen_balance() + { + let (info, fee_result) = platform.drive.fetch_identity_token_info_with_costs( + self.token_id().to_buffer(), + self.recipient_id().to_buffer(), + block_info, + true, + transaction, + platform_version, + )?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + if let Some(info) = info { + if info.frozen() { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::IdentityTokenAccountFrozenError( + IdentityTokenAccountFrozenError::new( + self.token_id(), + self.recipient_id(), + "transfer".to_string(), + ), + )), + )); + } + }; + } + // We need to verify that the token is not paused let (token_status, fee_result) = platform.drive.fetch_token_status_with_costs( self.token_id().to_buffer(), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/mod.rs index cc7a4b948f0..369c90a684c 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/mod.rs @@ -14,7 +14,6 @@ use dpp::data_contract::associated_token::token_configuration::accessors::v0::To use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Setters; use dpp::data_contract::associated_token::token_configuration::TokenConfiguration; use dpp::data_contract::associated_token::token_configuration_convention::v0::TokenConfigurationConventionV0; -use dpp::data_contract::associated_token::token_configuration_localization::TokenConfigurationLocalization; use dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Setters; use dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; use dpp::data_contract::change_control_rules::v0::ChangeControlRulesV0; @@ -4350,6 +4349,194 @@ mod token_tests { let expected_amount = 1337 - 300; assert_eq!(token_balance, Some(expected_amount)); } + + #[test] + fn test_token_frozen_receive_balance_may_not_be_allowed() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(49853); + + let platform_state = platform.state.load(); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (recipient, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration.allow_transfer_to_frozen_balance(false); + + token_configuration.set_freeze_rules(ChangeControlRules::V0( + ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::ContractOwner, + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }, + )); + token_configuration.set_unfreeze_rules(ChangeControlRules::V0( + ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::ContractOwner, + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }, + )); + }), + None, + None, + platform_version, + ); + + let freeze_transition = BatchTransition::new_token_freeze_transition( + token_id, + identity.id(), + contract.id(), + 0, + recipient.id(), + None, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let freeze_serialized_transition = freeze_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![freeze_serialized_transition.clone()], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_frozen = platform + .drive + .fetch_identity_token_info( + token_id.to_buffer(), + recipient.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token info") + .map(|info| info.frozen()); + assert_eq!(token_frozen, Some(true)); + + let token_transfer_transition = BatchTransition::new_token_transfer_transition( + token_id, + identity.id(), + contract.id(), + 0, + 1337, + recipient.id(), + None, + None, + None, + &key, + 3, + 0, + &signer, + platform_version, + None, + None, + None, + ) + .expect("expect to create documents batch transition"); + + let token_transfer_serialized_transition = token_transfer_transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &vec![token_transfer_serialized_transition.clone()], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError( + ConsensusError::StateError(StateError::IdentityTokenAccountFrozenError(_)), + _ + )] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + let expected_amount = 100000; + assert_eq!(token_balance, Some(expected_amount)); + + let token_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + recipient.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(token_balance, None); + } } mod token_config_update_tests { @@ -6066,13 +6253,12 @@ mod token_tests { TokenConfigurationConventionV0 { localizations: [( "en".to_string(), - TokenConfigurationLocalization::V0( - TokenConfigurationLocalizationV0 { - should_capitalize: true, - singular_form: "garzon".to_string(), - plural_form: "garzons".to_string(), - }, - ), + TokenConfigurationLocalizationV0 { + should_capitalize: true, + singular_form: "garzon".to_string(), + plural_form: "garzons".to_string(), + } + .into(), )] .into(), decimals: 8, diff --git a/packages/rs-drive-abci/tests/strategy_tests/token_tests.rs b/packages/rs-drive-abci/tests/strategy_tests/token_tests.rs index d2a2607c92d..21ab578db9f 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/token_tests.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/token_tests.rs @@ -152,7 +152,7 @@ mod tests { ) .expect("expected to get balances"); - for (identity_id, token_balance) in balances { + for (_identity_id, token_balance) in balances { assert!(token_balance.is_some()) }