diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs index eaac0f4e5f2..9758e43edc9 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs @@ -1049,6 +1049,273 @@ mod token_burn_tests { assert_eq!(total_supply_after, Some(75000)); } + #[tokio::test] + async fn test_token_burn_below_base_supply_allowed() { + // base_supply is purely the INITIAL mint at contract creation; there is no guard + // anywhere preventing burns from dropping total_supply below base_supply. This is + // allowed by design. This test locks in that invariant: burning + // 60_000 from a base supply of 100_000 leaves a total of 40_000, below base. + 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 (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + None::, + None, + None, + None, + platform_version, + ); + + let total_supply_before = platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("expected to fetch total supply"); + assert_eq!(total_supply_before, Some(100000)); + + let burn_transition = BatchTransition::new_token_burn_transition( + token_id, + identity.id(), + contract.id(), + 0, + 60000, + None, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expect to create burn transition"); + + let serialized = burn_transition + .serialize_to_bytes() + .expect("expected serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[serialized], + &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"); + + // Supply dropped below the original base_supply (100_000) and that is allowed. + let balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch balance"); + assert_eq!(balance, Some(40000)); + + let total_supply_after = platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("expected to fetch total supply"); + assert_eq!(total_supply_after, Some(40000)); + } + + #[tokio::test] + async fn test_token_burn_entire_supply_then_mint_again() { + // Burning the entire supply to zero must leave the supply entry at Some(0) (not + // absent), so a subsequent mint resumes correctly rather than hitting a + // CorruptedDriveState "total supply not found". Existing depletion coverage only + // checks that a *further burn* fails; this checks that a mint after depletion + // works. + 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 (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + None::, + None, + None, + None, + platform_version, + ); + + // Burn the entire base supply (100_000) to zero. + let burn_transition = BatchTransition::new_token_burn_transition( + token_id, + identity.id(), + contract.id(), + 0, + 100000, + None, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expect to create burn transition"); + + let serialized = burn_transition + .serialize_to_bytes() + .expect("expected serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[serialized], + &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 balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch balance"); + assert_eq!(balance, Some(0)); + + let total_supply = platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("expected to fetch total supply"); + assert_eq!(total_supply, Some(0)); + + // Now mint again from the depleted (but present) supply entry. + let mint_transition = BatchTransition::new_token_mint_transition( + token_id, + identity.id(), + contract.id(), + 0, + 500, + Some(identity.id()), + None, + None, + &key, + 3, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expect to create mint transition"); + + let serialized = mint_transition + .serialize_to_bytes() + .expect("expected serialized state transition"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[serialized], + &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 total_supply = platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("expected to fetch total supply"); + assert_eq!(total_supply, Some(500)); + + let balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch balance"); + assert_eq!(balance, Some(500)); + } + #[tokio::test] async fn test_token_burn_with_public_note_succeeds() { let platform_version = PlatformVersion::latest(); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/config_update/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/config_update/mod.rs index a34403210f4..da68d055859 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/config_update/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/config_update/mod.rs @@ -372,6 +372,363 @@ mod token_config_update_tests { assert_eq!(updated_token_config.max_supply(), None); } + #[tokio::test] + async fn test_token_config_update_set_max_supply_equal_to_current_supply_succeeds() { + // The max-supply check rejects only max_supply < current_supply (a strict `>` + // comparison in the token config-update state validation). Setting max_supply + // to EXACTLY the current supply must therefore be allowed. The existing + // coverage only checks the rejected (max < current) side. + 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)); + + // base_supply 100_000 (so current supply is 100_000), owner may change max. + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration.set_max_supply_change_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, + None, + platform_version, + ); + + // Prove the precondition rather than relying on the fixture default: the + // current supply must be exactly 100_000 before we set max_supply to it. + let current_supply = platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("expected to fetch total supply"); + assert_eq!(current_supply, Some(100000)); + + // Set max_supply to exactly the current supply (100_000). + let config_update_transition = BatchTransition::new_token_config_update_transition( + token_id, + identity.id(), + contract.id(), + 0, + TokenConfigurationChangeItem::MaxSupply(Some(100000)), + None, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expect to create config update transition"); + + let serialized = config_update_transition + .serialize_to_bytes() + .expect("expected to serialize"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[serialized], + &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 contract = platform + .drive + .fetch_contract( + contract.id().to_buffer(), + None, + None, + None, + platform_version, + ) + .unwrap() + .expect("expected to fetch contract") + .expect("expected contract"); + let updated_token_config = contract + .contract + .expected_token_configuration(0) + .expect("expected token configuration"); + assert_eq!(updated_token_config.max_supply(), Some(100000)); + } + + #[tokio::test] + async fn test_token_config_update_raise_max_supply_then_mint_into_headroom() { + // A valid token sitting at its cap (base_supply == max_supply == 100_000) + // cannot mint until max_supply is raised. This pins the intended expansion + // path: a mint at the cap is rejected, the owner raises max_supply, and a + // subsequent mint into the new headroom succeeds. + 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)); + + // Valid token: base_supply == max_supply, so it starts exactly at the cap. + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration.set_max_supply(Some(100000)); + token_configuration.set_max_supply_change_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, + None, + platform_version, + ); + + // Prove the precondition: the token starts exactly at its cap (100_000). + let current_supply = platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("expected to fetch total supply"); + assert_eq!(current_supply, Some(100000)); + + // A mint of 1 at the cap is rejected. + let mint_blocked = BatchTransition::new_token_mint_transition( + token_id, + identity.id(), + contract.id(), + 0, + 1, + Some(identity.id()), + None, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expect to create mint transition"); + + let serialized = mint_blocked + .serialize_to_bytes() + .expect("expected to serialize"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[serialized], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + // Assert the full payload: minting 1 when current_supply == max_supply == 100_000. + let results = processing_result.execution_results(); + assert_matches!( + results.as_slice(), + [PaidConsensusError { + error: ConsensusError::StateError(StateError::TokenMintPastMaxSupplyError(_)), + .. + }] + ); + let PaidConsensusError { + error: ConsensusError::StateError(StateError::TokenMintPastMaxSupplyError(err)), + .. + } = &results[0] + else { + unreachable!("asserted TokenMintPastMaxSupplyError above"); + }; + assert_eq!(err.amount(), 1); + assert_eq!(err.current_supply(), 100000); + assert_eq!(err.max_supply(), 100000); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + // Raise max_supply to 200_000. + let raise_max = BatchTransition::new_token_config_update_transition( + token_id, + identity.id(), + contract.id(), + 0, + TokenConfigurationChangeItem::MaxSupply(Some(200000)), + None, + None, + &key, + 3, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expect to create config update transition"); + + let serialized = raise_max + .serialize_to_bytes() + .expect("expected to serialize"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[serialized], + &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"); + + // Directly confirm the config was raised (the mint below also proves it, but + // this test advertises the raise, so assert it explicitly). + let updated_contract = platform + .drive + .fetch_contract( + contract.id().to_buffer(), + None, + None, + None, + platform_version, + ) + .unwrap() + .expect("expected to fetch contract") + .expect("expected contract"); + let updated_token_config = updated_contract + .contract + .expected_token_configuration(0) + .expect("expected token configuration"); + assert_eq!(updated_token_config.max_supply(), Some(200000)); + + // A mint into the new headroom now succeeds. + let mint_ok = BatchTransition::new_token_mint_transition( + token_id, + identity.id(), + contract.id(), + 0, + 10000, + Some(identity.id()), + None, + None, + &key, + 4, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expect to create mint transition"); + + let serialized = mint_ok.serialize_to_bytes().expect("expected to serialize"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[serialized], + &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 total_supply = platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("expected to fetch total supply"); + assert_eq!(total_supply, Some(110000)); + } + #[tokio::test] async fn test_token_config_update_by_owner_change_admin_to_another_identity() { let platform_version = PlatformVersion::latest(); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/destroy_frozen_funds/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/destroy_frozen_funds/mod.rs index be96d075f70..efbebd8771d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/destroy_frozen_funds/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/destroy_frozen_funds/mod.rs @@ -2,6 +2,10 @@ use super::*; mod token_destroy_frozen_funds_tests { use super::*; + use crate::execution::validation::state_transition::tests::process_test_state_transition; + use crate::platform_types::platform_state::PlatformState; + use crate::rpc::core::MockCoreRPCLike; + use crate::test::helpers::setup::TempPlatform; use dpp::tokens::info::v0::IdentityTokenInfoV0Accessors; #[tokio::test] @@ -406,4 +410,407 @@ mod token_destroy_frozen_funds_tests { }] ); } + + #[tokio::test] + async fn test_token_destroy_frozen_funds_to_zero_total_supply() { + // Destroying the frozen funds of the sole holder must bring total_supply to + // exactly 0. Existing destroy tests never assert total supply. base_supply is 0 + // and the entire supply (5000) is minted to identity_2, so identity_2's frozen + // balance equals the whole supply. + 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 (identity_2, _, _) = 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.set_base_supply(0); + token_configuration.set_destroy_frozen_funds_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_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_manual_minting_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 + .distribution_rules_mut() + .set_minting_allow_choosing_destination(true); + }), + None, + None, + None, + platform_version, + ); + + // With base_supply 0, total supply starts at 0. + let total_supply_before = platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("expected to fetch total supply"); + assert_eq!(total_supply_before, Some(0)); + + // Mint the entire supply to identity_2. + let mint_transition = BatchTransition::new_token_mint_transition( + token_id, + identity.id(), + contract.id(), + 0, + 5000, + Some(identity_2.id()), + None, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expected to create mint transition"); + + process_and_commit_success( + &mut platform, + &platform_state, + mint_transition, + platform_version, + ); + + let total_supply = platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("expected to fetch total supply"); + assert_eq!(total_supply, Some(5000)); + + // Freeze identity_2. + let freeze_transition = BatchTransition::new_token_freeze_transition( + token_id, + identity.id(), + contract.id(), + 0, + identity_2.id(), + None, + None, + &key, + 3, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expected to create freeze transition"); + + process_and_commit_success( + &mut platform, + &platform_state, + freeze_transition, + platform_version, + ); + + // Destroy identity_2's frozen funds. + let destroy_transition = BatchTransition::new_token_destroy_frozen_funds_transition( + token_id, + identity.id(), + contract.id(), + 0, + identity_2.id(), + None, + None, + &key, + 4, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expected to create destroy frozen funds transition"); + + process_and_commit_success( + &mut platform, + &platform_state, + destroy_transition, + platform_version, + ); + + // identity_2 balance is now 0 and total supply is 0. + let balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(balance, Some(0)); + + let total_supply = platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("expected to fetch total supply"); + assert_eq!(total_supply, Some(0)); + + // identity_2 is still frozen after destruction. + let token_frozen = platform + .drive + .fetch_identity_token_info( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token info") + .map(|info| info.frozen()); + assert_eq!(token_frozen, Some(true)); + } + + #[tokio::test] + async fn test_token_destroy_frozen_funds_decrements_total_supply() { + // Destroying a frozen holder's funds must decrement total_supply by exactly the + // destroyed amount, leaving other holders untouched. base_supply 0; mint 5000 to + // identity_2 and 3000 to the owner (total 8000); destroying identity_2's 5000 + // must leave total supply at 3000. + 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 (identity_2, _, _) = 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.set_base_supply(0); + token_configuration.set_destroy_frozen_funds_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_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_manual_minting_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 + .distribution_rules_mut() + .set_minting_allow_choosing_destination(true); + }), + None, + None, + None, + platform_version, + ); + + // Mint 5000 to identity_2. + let mint_to_2 = BatchTransition::new_token_mint_transition( + token_id, + identity.id(), + contract.id(), + 0, + 5000, + Some(identity_2.id()), + None, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expected to create mint transition"); + + process_and_commit_success(&mut platform, &platform_state, mint_to_2, platform_version); + + // Mint 3000 to the owner. + let mint_to_owner = BatchTransition::new_token_mint_transition( + token_id, + identity.id(), + contract.id(), + 0, + 3000, + Some(identity.id()), + None, + None, + &key, + 3, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expected to create mint transition"); + + process_and_commit_success( + &mut platform, + &platform_state, + mint_to_owner, + platform_version, + ); + + let total_supply_before = platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("expected to fetch total supply"); + assert_eq!(total_supply_before, Some(8000)); + + // Freeze identity_2. + let freeze_transition = BatchTransition::new_token_freeze_transition( + token_id, + identity.id(), + contract.id(), + 0, + identity_2.id(), + None, + None, + &key, + 4, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expected to create freeze transition"); + + process_and_commit_success( + &mut platform, + &platform_state, + freeze_transition, + platform_version, + ); + + // Destroy identity_2's frozen funds (5000). + let destroy_transition = BatchTransition::new_token_destroy_frozen_funds_transition( + token_id, + identity.id(), + contract.id(), + 0, + identity_2.id(), + None, + None, + &key, + 5, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expected to create destroy frozen funds transition"); + + process_and_commit_success( + &mut platform, + &platform_state, + destroy_transition, + platform_version, + ); + + // identity_2 emptied; total supply decremented by exactly 5000 (8000 -> 3000); + // owner balance untouched. + let balance_2 = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_2.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(balance_2, Some(0)); + + let balance_owner = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(balance_owner, Some(3000)); + + let total_supply_after = platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("expected to fetch total supply"); + assert_eq!(total_supply_after, Some(3000)); + } + + // Helper: process a single state transition, assert it succeeded, and commit. + // Delegates to the shared process_test_state_transition helper (commits internally). + fn process_and_commit_success( + platform: &mut TempPlatform, + platform_state: &PlatformState, + transition: S, + platform_version: &PlatformVersion, + ) { + let processing_result = + process_test_state_transition(platform, transition, platform_state, platform_version); + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/mint/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/mint/mod.rs index 0e6f1737710..88df5865efc 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/mint/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/mint/mod.rs @@ -271,6 +271,703 @@ mod token_mint_tests { assert_eq!(token_balance, Some(100000)); } + #[tokio::test] + async fn test_token_mint_to_exact_max_supply_succeeds() { + // The max-supply check uses a strict `>` comparison + // (token_mint_transition_action/state_v0/mod.rs), so minting to *exactly* + // max_supply must be allowed. base_supply is 100_000, max_supply 1_000_000, + // so minting 900_000 brings total to exactly 1_000_000. + 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 (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration.set_max_supply(Some(1000000)); + }), + None, + None, + None, + platform_version, + ); + + let mint_transition = BatchTransition::new_token_mint_transition( + token_id, + identity.id(), + contract.id(), + 0, + 900000, + Some(identity.id()), + None, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expect to create mint transition"); + + let serialized = mint_transition + .serialize_to_bytes() + .expect("expected to serialize"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[serialized], + &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_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(token_balance, Some(1000000)); + + let total_supply = platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("expected to fetch total supply"); + assert_eq!(total_supply, Some(1000000)); + } + + #[tokio::test] + async fn test_token_mint_one_over_max_supply_fails() { + // Off-by-one on the other side of the boundary: minting one token past + // max_supply must be rejected. base_supply 100_000, max_supply 1_000_000, + // minting 900_001 would bring total to 1_000_001. + 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 (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration.set_max_supply(Some(1000000)); + }), + None, + None, + None, + platform_version, + ); + + let mint_transition = BatchTransition::new_token_mint_transition( + token_id, + identity.id(), + contract.id(), + 0, + 900001, + Some(identity.id()), + None, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expect to create mint transition"); + + let serialized = mint_transition + .serialize_to_bytes() + .expect("expected to serialize"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[serialized], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + // Assert the full error payload — the point of a boundary test is the exact + // supply math, so we check amount / current_supply / max_supply, not just the + // variant. + let results = processing_result.execution_results(); + assert_matches!( + results.as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::TokenMintPastMaxSupplyError(_)), + .. + }] + ); + let StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::TokenMintPastMaxSupplyError(err)), + .. + } = &results[0] + else { + unreachable!("asserted TokenMintPastMaxSupplyError above"); + }; + assert_eq!(err.amount(), 900001); + assert_eq!(err.current_supply(), 100000); + assert_eq!(err.max_supply(), 1000000); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + // Supply and balance must be unchanged (still the base supply). + 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"); + assert_eq!(token_balance, Some(100000)); + + let total_supply = platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("expected to fetch total supply"); + assert_eq!(total_supply, Some(100000)); + } + + #[tokio::test] + async fn test_token_mint_unbounded_when_max_supply_none() { + // When max_supply is None there is NO upper-bound check at the ABCI layer + // (only Drive's i64::MAX guard applies). A very large mint that stays well + // under i64::MAX must therefore succeed. This documents the intended + // unbounded behavior. base_supply is 100_000. + 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)); + + // Default config leaves max_supply == None. + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + None::, + None, + None, + None, + platform_version, + ); + + let mint_amount = 1_000_000_000_000u64; + let mint_transition = BatchTransition::new_token_mint_transition( + token_id, + identity.id(), + contract.id(), + 0, + mint_amount, + Some(identity.id()), + None, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expect to create mint transition"); + + let serialized = mint_transition + .serialize_to_bytes() + .expect("expected to serialize"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[serialized], + &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 expected = 100000 + mint_amount; + let total_supply = platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("expected to fetch total supply"); + assert_eq!(total_supply, Some(expected)); + } + + #[tokio::test] + async fn test_token_mint_from_zero_base_supply() { + // A token created with base_supply == 0 must initialize its total supply + // entry to 0 (not leave it absent). If the entry were missing, mint + // validation would fail with CorruptedDriveState. After a mint, supply and + // balance must reflect the minted amount. + 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 (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration.set_base_supply(0); + }), + None, + None, + None, + platform_version, + ); + + // Initial supply must be Some(0), not None. + let total_supply_before = platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("expected to fetch total supply"); + assert_eq!(total_supply_before, Some(0)); + + let mint_transition = BatchTransition::new_token_mint_transition( + token_id, + identity.id(), + contract.id(), + 0, + 1337, + Some(identity.id()), + None, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expect to create mint transition"); + + let serialized = mint_transition + .serialize_to_bytes() + .expect("expected to serialize"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[serialized], + &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_balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch token balance"); + assert_eq!(token_balance, Some(1337)); + + let total_supply_after = platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("expected to fetch total supply"); + assert_eq!(total_supply_after, Some(1337)); + } + + #[tokio::test] + async fn test_token_mint_from_zero_base_supply_to_exact_max() { + // Boundary logic must hold starting from a zero base supply: mint to exactly + // max_supply succeeds, and a further mint of 1 is rejected. + 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 (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration.set_base_supply(0); + token_configuration.set_max_supply(Some(1000)); + }), + None, + None, + None, + platform_version, + ); + + // First mint to exactly max_supply. + let mint_transition = BatchTransition::new_token_mint_transition( + token_id, + identity.id(), + contract.id(), + 0, + 1000, + Some(identity.id()), + None, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expect to create mint transition"); + + let serialized = mint_transition + .serialize_to_bytes() + .expect("expected to serialize"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[serialized], + &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 total_supply = platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("expected to fetch total supply"); + assert_eq!(total_supply, Some(1000)); + + // Second mint of 1 must be rejected as past max supply. + let mint_transition_2 = BatchTransition::new_token_mint_transition( + token_id, + identity.id(), + contract.id(), + 0, + 1, + Some(identity.id()), + None, + None, + &key, + 3, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expect to create second mint transition"); + + let serialized_2 = mint_transition_2 + .serialize_to_bytes() + .expect("expected to serialize"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[serialized_2], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + // Assert the full payload: minting 1 when current_supply == max_supply == 1000. + let results = processing_result.execution_results(); + assert_matches!( + results.as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::TokenMintPastMaxSupplyError(_)), + .. + }] + ); + let StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::TokenMintPastMaxSupplyError(err)), + .. + } = &results[0] + else { + unreachable!("asserted TokenMintPastMaxSupplyError above"); + }; + assert_eq!(err.amount(), 1); + assert_eq!(err.current_supply(), 1000); + assert_eq!(err.max_supply(), 1000); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + let total_supply = platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("expected to fetch total supply"); + assert_eq!(total_supply, Some(1000)); + } + + #[tokio::test] + async fn test_token_mint_with_max_i64_base_supply_then_overflow_returns_internal_error_without_mutating_supply( + ) { + // CHARACTERIZATION TEST (current behavior, not the desired long-term API). + // + // A contract may be created with base_supply == i64::MAX (the largest value + // that passes the Drive sum-item guard). A subsequent mint of 1 would push the + // supply past i64::MAX. With max_supply == None there is no validation-layer + // guard, so this is only caught by the low-level Drive checked_add and surfaces + // as an InternalError (the "corrupted execution" class) rather than a graceful + // consensus rejection — while leaving supply unmutated. + // + // This test pins that current shape. When a validation-layer guard is added + // (tracked separately), this test SHOULD break: that is the signal to update it + // from the characterized-current behavior to the new graceful-rejection behavior. + 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 base = i64::MAX as u64; + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(move |token_configuration: &mut TokenConfiguration| { + token_configuration.set_base_supply(base); + }), + None, + None, + None, + platform_version, + ); + + // Creation initializes the supply to i64::MAX. + let total_supply_before = platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("expected to fetch total supply"); + assert_eq!(total_supply_before, Some(base)); + + let mint_transition = BatchTransition::new_token_mint_transition( + token_id, + identity.id(), + contract.id(), + 0, + 1, + Some(identity.id()), + None, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expect to create mint transition"); + + let serialized = mint_transition + .serialize_to_bytes() + .expect("expected to serialize"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[serialized], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + // Minting 1 past an i64::MAX supply is caught by the Drive sum-item guard + // (checked_add in add_to_token_total_supply_operations_v0). With + // max_supply == None there is no graceful consensus-level rejection, so it + // surfaces as an InternalError carrying the Drive overflow message rather + // than a clean PaidConsensusError. We assert that concrete shape (not merely + // "not successful") so the test fails loudly if the surfaced result changes. + let results = processing_result.execution_results(); + assert_matches!( + results.as_slice(), + [StateTransitionExecutionResult::InternalError(_)] + ); + let StateTransitionExecutionResult::InternalError(message) = &results[0] else { + unreachable!("asserted InternalError above"); + }; + assert!( + message.contains("overflow total supply"), + "expected the Drive overflow guard message, got: {message}" + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + // Supply must be unchanged. + let total_supply_after = platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("expected to fetch total supply"); + assert_eq!(total_supply_after, Some(base)); + } + + // NOTE: the base_supply > max_supply creation gap is documented by a real + // validation-path test: + // data_contract_create::tests::tokens::token_errors:: + // test_data_contract_creation_with_base_supply_over_max_supply_should_cause_error + // (it runs an actual DataContractCreateTransition, unlike the setup_contract + // helper used here, which bypasses state-transition validation). + #[tokio::test] async fn test_token_mint_by_owner_allowed_sending_to_other() { let platform_version = PlatformVersion::latest(); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs index ba541b1ccd7..bebb5a64dcb 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs @@ -1826,6 +1826,112 @@ mod tests { assert_eq!(token_balance, None); } + #[tokio::test] + #[ignore = "documents missing creation-time guard: base_supply > max_supply is currently allowed (no production guard exists). Such a token is created already over its cap, base_supply is immutable, and every mint is then blocked by TokenMintPastMaxSupplyError. Remove #[ignore] once the guard is added in data_contract_create/basic_structure (alongside the base_supply > i64::MAX check)."] + async fn test_data_contract_creation_with_base_supply_over_max_supply_should_cause_error( + ) { + // INTENDED behavior: a contract whose base_supply exceeds its own + // max_supply must be REJECTED at creation. Today there is no guard + // comparing the two (the create validator only rejects + // base_supply > i64::MAX, data_contract_create/basic_structure/v0/mod.rs), + // so this currently FAILS: the contract is created with total supply equal + // to base_supply, already over the cap. This is the real validation-path + // analogue of the gap (it runs an actual DataContractCreateTransition + // through process_raw_state_transitions, not the setup_contract helper). + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let platform_state = platform.state.load(); + + let (identity, signer, key) = + setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let mut data_contract = json_document_to_contract_with_ids( + "tests/supporting_files/contract/basic-token/basic-token.json", + None, + None, + false, //no need to validate the data contracts in tests for drive + platform_version, + ) + .expect("expected to get json based contract"); + + { + let token_config = data_contract + .tokens_mut() + .expect("expected tokens") + .get_mut(&0) + .expect("expected first token"); + // base_supply (100_000) exceeds max_supply (50_000): inconsistent. + token_config.set_base_supply(100000); + token_config.set_max_supply(Some(50000)); + } + + let identity_id = identity.id(); + + let data_contract_id = DataContract::generate_data_contract_id_v0(identity_id, 1); + + let data_contract_create_transition = + DataContractCreateTransition::new_from_data_contract( + data_contract, + 1, + &identity.into_partial_identity_info(), + key.id(), + &signer, + platform_version, + None, + ) + .await + .expect("expect to create data contract create transition"); + + let token_id = calculate_token_id(data_contract_id.as_bytes(), 0); + + let serialized = data_contract_create_transition + .serialize_to_bytes() + .expect("expected to serialize"); + + let transaction = platform.drive.grove.start_transaction(); + + let processing_result = platform + .platform + .process_raw_state_transitions( + &[serialized], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + + // INTENDED: creation is rejected during basic structure validation, + // before paid execution can run. + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::BasicError(_) + )] + ); + + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + // INTENDED: the token must not exist, so its supply is absent (not the + // over-cap value). This is non-vacuous: today the token IS created with + // supply Some(100000), failing this assertion. + let total_supply = platform + .drive + .fetch_token_total_supply(token_id, None, platform_version) + .expect("expected to fetch total supply"); + assert_eq!(total_supply, None); + } + #[tokio::test] async fn test_data_contract_creation_with_single_token_needing_group_that_does_not_exist( ) {