From d18e72551187e08a4704b3dbc86e9936a2dc36ab Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sat, 19 Sep 2026 07:31:01 +0700 Subject: [PATCH 1/2] fix(drive)!: mint base supply of tokens added by contract update `insert_contract` v1 credits a token's base supply to its `new_tokens_destination_identity` (the contract owner when unset) and starts the total supply at that amount. The update path only called `create_token_trees_operations`, which starts the supply at zero and writes no balance, although `validate_update_tokens` lets an update add tokens. A token added by update with a base supply therefore executed as a paid success and came into existence with nothing minted. `update_contract` v2, selected by protocol version 14 only and still unreleased, now mints the base supply of tokens absent from the original contract. The zero total supply insert queued by `create_token_trees_operations` is replaced instead of followed by a second operation on the same path and key, which the batch consistency check rejects. Tokens the contract already had are left alone, so a token config update and later contract updates never mint again, and an update can not remove a token, so a token is new exactly once. A base supply over `i64::MAX` is refused as in the insert. The update's basic structure validation already rejects it as a consensus error, so the internal error is not reachable from a state transition. Tokens added by update before protocol version 14 are not minted retroactively. The claim tests of tokens added by update now use a token without base supply, so the asserted balance is what the claim paid on every protocol version. Co-Authored-By: Claude Fable 5.1 --- .../data_contract_update/mod.rs | 146 ++++++ .../contract/update/update_contract/v2/mod.rs | 461 +++++++++++++++++- .../drive_contract_method_versions/v4.rs | 7 + .../rs-platform-version/src/version/v14.rs | 10 + 4 files changed, 620 insertions(+), 4 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs index a212e619dcc..acd0d8452f9 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs @@ -2747,6 +2747,10 @@ mod tests { let mut token_configuration = TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + // No base supply, so the owner's balance is what the claim paid on + // every protocol version: from version 14 the update mints the base + // supply of a token it adds, to the owner here. + token_configuration.set_base_supply(0); token_configuration.set_conventions(TokenConfigurationConvention::V0( TokenConfigurationConventionV0 { localizations: BTreeMap::from([( @@ -2968,6 +2972,148 @@ mod tests { assert_eq!(token_balance, None); } } + + /// Registers a contract without tokens and adds a token with a base + /// supply of 1 000 000 at position 0 through a data contract update, + /// both under `protocol_version`. Returns the contract owner's resulting + /// balance of the token and the token's total supply. + async fn add_token_with_base_supply_by_update( + protocol_version: ProtocolVersion, + ) -> (Option, Option) { + let platform_version = + PlatformVersion::get(protocol_version).expect("expected a known protocol version"); + let mut platform = TestPlatformBuilder::new() + .with_initial_protocol_version(protocol_version) + .build_with_mock_rpc() + .set_initial_state_structure(); + + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let platform_state = platform.state.load(); + + let mut data_contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + data_contract.set_owner_id(identity.id()); + + platform + .drive + .apply_contract( + &data_contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply contract successfully"); + + let mut updated_data_contract = data_contract.clone(); + updated_data_contract.set_version(2); + + let mut token_configuration = + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + token_configuration.set_base_supply(1_000_000); + token_configuration.set_conventions(TokenConfigurationConvention::V0( + TokenConfigurationConventionV0 { + localizations: BTreeMap::from([( + "en".to_string(), + TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 { + should_capitalize: true, + singular_form: "credit".to_string(), + plural_form: "credits".to_string(), + }), + )]), + decimals: 8, + }, + )); + updated_data_contract.add_token(0, token_configuration); + + let token_id = updated_data_contract + .token_id(0) + .expect("expected the token added at position 0"); + + let data_contract_update_transition = + DataContractUpdateTransition::new_from_data_contract( + updated_data_contract, + &identity.clone().into_partial_identity_info(), + key.id(), + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expect to create data contract update transition"); + + let update_bytes = data_contract_update_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( + &[update_bytes], + &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 { .. }], + "the update adding the token must succeed on every protocol version" + ); + + 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 the token balance"); + let total_supply = platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("expected to fetch the token total supply"); + + (token_balance, total_supply) + } + + #[tokio::test] + async fn should_mint_base_supply_of_token_added_by_update() { + let latest = PlatformVersion::latest().protocol_version; + + assert_eq!( + add_token_with_base_supply_by_update(latest).await, + (Some(1_000_000), Some(1_000_000)) + ); + } + + /// The frozen side of the gate: on protocol version 13 the update + /// creates the token with a total supply of zero and credits nobody. + #[tokio::test] + async fn should_not_mint_base_supply_of_token_added_by_update_on_protocol_version_13() { + assert_eq!( + add_token_with_base_supply_by_update(13).await, + (None, Some(0)) + ); + } } mod keyword_updates { diff --git a/packages/rs-drive/src/drive/contract/update/update_contract/v2/mod.rs b/packages/rs-drive/src/drive/contract/update/update_contract/v2/mod.rs index 310539769b7..a07af72d619 100644 --- a/packages/rs-drive/src/drive/contract/update/update_contract/v2/mod.rs +++ b/packages/rs-drive/src/drive/contract/update/update_contract/v2/mod.rs @@ -11,13 +11,17 @@ use dpp::fee::fee_result::FeeResult; use dpp::serialization::PlatformSerializableWithPlatformVersion; +use crate::drive::balances::total_tokens_root_supply_path_vec; +use crate::drive::tokens::paths::token_balances_path_vec; use crate::error::contract::DataContractError; +use dpp::balances::credits::TokenAmount; use dpp::data_contract::accessors::v1::DataContractV1Getters; use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; use dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; use dpp::data_contract::associated_token::token_distribution_rules::accessors::v1::TokenDistributionRulesV1Getters; use dpp::fee::default_costs::CachedEpochIndexFeeVersions; use dpp::version::PlatformVersion; +use dpp::ProtocolError; use grovedb::batch::KeyInfoPath; use grovedb::{Element, EstimatedLayerInformation, TransactionArg}; use std::collections::HashMap; @@ -208,6 +212,10 @@ impl Drive { /// created none of it, so the first claim on such a token wrote its claim /// record under a tree that did not exist and failed as an internal error, /// leaving the distribution unclaimable. + /// + /// It also mints the base supply of a token the update adds, to the same + /// identity `insert_contract` credits at registration. v1 left such a + /// token at a total supply of zero with nobody holding any of it. #[allow(clippy::too_many_arguments)] fn update_contract_operations_v2( &self, @@ -234,10 +242,13 @@ impl Drive { for (token_pos, configuration) in contract.tokens() { // Only a token absent from the original contract is new to state. - // A token the contract already had keeps the distribution storage - // it has, and both helpers error when the token's tree already - // exists. That covers a token config update too, which reaches - // this method with its token present in the original contract. + // A token the contract already had keeps the supply and the + // distribution storage it has: its base supply was minted when it + // was added, and both distribution helpers error when the token's + // tree already exists. That covers a token config update too, + // which reaches this method with its token present in the original + // contract. An update can not remove a token, so a token is new + // exactly once. if original_contract.tokens().contains_key(token_pos) { continue; } @@ -249,6 +260,21 @@ impl Drive { )), ))?; + if configuration.base_supply() > 0 { + let destination_identity_id = configuration + .distribution_rules() + .new_tokens_destination_identity() + .copied() + .unwrap_or(contract.owner_id()); + + Self::mint_base_supply_of_added_token( + token_id.to_buffer(), + configuration.base_supply(), + destination_identity_id.to_buffer(), + &mut batch_operations, + )?; + } + if let Some(perpetual_distribution) = configuration.distribution_rules().perpetual_distribution() { @@ -298,12 +324,77 @@ impl Drive { Ok(batch_operations) } + + /// Turns the operations creating the trees of a token the update adds into + /// ones that also mint its base supply, the way `insert_contract` v1 does + /// at registration: `destination_identity_id` is credited `base_supply` + /// and the token's total supply starts at `base_supply`. + /// + /// The v1 operations already hold the insert of a zero + /// total supply, and a batch may hold only one operation per path and + /// key, so that insert is replaced rather than followed by a second one. + /// The supply helpers used by a mint transition do not fit here: they + /// read the current supply and balance from state, where this token's + /// trees do not exist until the batch is applied. + fn mint_base_supply_of_added_token( + token_id: [u8; 32], + base_supply: TokenAmount, + destination_identity_id: [u8; 32], + token_operations: &mut Vec, + ) -> Result<(), Error> { + // The update transition's basic structure validation rejects such a + // base supply as a consensus error, so this is not reachable from a + // state transition. + if base_supply > i64::MAX as u64 { + return Err( + ProtocolError::CriticalCorruptedCreditsCodeExecution(format!( + "Token base supply over i64 max, is {}", + base_supply + )) + .into(), + ); + } + + let zero_total_supply_insert = LowLevelDriveOperation::insert_for_known_path_key_element( + total_tokens_root_supply_path_vec(), + token_id.to_vec(), + Element::new_sum_item(0), + ); + + // The token is new to the contract and a token id is unique to its + // contract and position, so no supply entry can exist yet and the + // insert has to be there. Minting on top of an existing entry would + // overwrite a live supply. + let total_supply_insert = token_operations + .iter_mut() + .find(|operation| **operation == zero_total_supply_insert) + .ok_or(Error::Drive(DriveError::CorruptedDriveState( + "a token new to the contract already has a total supply".to_string(), + )))?; + + *total_supply_insert = LowLevelDriveOperation::insert_for_known_path_key_element( + total_tokens_root_supply_path_vec(), + token_id.to_vec(), + Element::new_sum_item(base_supply as i64), + ); + + token_operations.push(LowLevelDriveOperation::insert_for_known_path_key_element( + token_balances_path_vec(token_id), + destination_identity_id.to_vec(), + Element::new_sum_item(base_supply as i64), + )); + + Ok(()) + } } #[cfg(test)] mod tests { + use crate::drive::balances::total_tokens_root_supply_path_vec; use crate::drive::Drive; + use crate::error::drive::DriveError; use crate::error::Error; + use crate::fees::op::LowLevelDriveOperation; use crate::util::storage_flags::StorageFlags; use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; use dpp::block::block_info::BlockInfo; @@ -328,6 +419,8 @@ mod tests { use dpp::prelude::{DataContract, Identifier}; use dpp::tests::fixtures::get_dashpay_contract_fixture; use dpp::version::PlatformVersion; + use dpp::ProtocolError; + use grovedb::Element; use std::collections::BTreeMap; const DISTRIBUTION_RECIPIENT: [u8; 32] = [7; 32]; @@ -676,4 +769,364 @@ mod tests { .expect("a pre-programmed claim should be recordable on both tokens"); } } + + const BASE_SUPPLY: u64 = 1_000_000; + const BASE_SUPPLY_DESTINATION: [u8; 32] = [9; 32]; + + /// A token with a base supply of `BASE_SUPPLY`, credited to `destination` + /// when there is one and to the contract owner otherwise. + fn token_with_base_supply(destination: Option) -> TokenConfiguration { + let mut configuration = TokenConfiguration::V0( + TokenConfigurationV0::default_most_restrictive().with_base_supply(BASE_SUPPLY), + ); + configuration + .distribution_rules_mut() + .set_new_tokens_destination_identity(destination); + configuration + } + + /// Registers a contract without tokens, then adds `configuration` at + /// position 0 through `update_contract`. Returns the updated contract and + /// the id of the added token. + fn add_token_by_update( + drive: &Drive, + configuration: TokenConfiguration, + platform_version: &PlatformVersion, + ) -> (DataContract, [u8; 32]) { + let mut contract = get_dashpay_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + contract.config_mut().set_readonly(false); + + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("insert initial contract without tokens"); + + contract.set_tokens(BTreeMap::from([(0, configuration)])); + contract.increment_version(); + + drive + .update_contract( + &contract, + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("update adding the token should succeed"); + + let token_id = contract + .token_id(0) + .expect("expected the token added at position 0") + .to_buffer(); + + (contract, token_id) + } + + fn balance_and_total_supply( + drive: &Drive, + token_id: [u8; 32], + identity_id: [u8; 32], + platform_version: &PlatformVersion, + ) -> (Option, Option) { + let balance = drive + .fetch_identity_token_balance(token_id, identity_id, None, platform_version) + .expect("expected to fetch the token balance"); + let total_supply = drive + .fetch_token_total_supply(token_id, None, platform_version) + .expect("expected to fetch the token total supply"); + (balance, total_supply) + } + + #[test] + fn should_mint_base_supply_to_contract_owner_for_token_added_by_update() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let (contract, token_id) = + add_token_by_update(&drive, token_with_base_supply(None), platform_version); + + assert_eq!( + balance_and_total_supply( + &drive, + token_id, + contract.owner_id().to_buffer(), + platform_version + ), + (Some(BASE_SUPPLY), Some(BASE_SUPPLY)) + ); + assert_eq!( + drive + .fetch_token_total_aggregated_identity_balances(token_id, None, platform_version) + .expect("expected to fetch the aggregated balances"), + Some(BASE_SUPPLY), + "the balances of the token must sum to its total supply" + ); + } + + #[test] + fn should_mint_base_supply_to_new_tokens_destination_identity_for_token_added_by_update() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let (contract, token_id) = add_token_by_update( + &drive, + token_with_base_supply(Some(Identifier::from(BASE_SUPPLY_DESTINATION))), + platform_version, + ); + + assert_eq!( + balance_and_total_supply(&drive, token_id, BASE_SUPPLY_DESTINATION, platform_version), + (Some(BASE_SUPPLY), Some(BASE_SUPPLY)) + ); + assert_eq!( + drive + .fetch_identity_token_balance( + token_id, + contract.owner_id().to_buffer(), + None, + platform_version + ) + .expect("expected to fetch the owner's token balance"), + None, + "the owner gets nothing when the token names another destination" + ); + } + + /// The frozen side of the gate, through the same dispatcher: protocol + /// version 13 selects v1, which creates the token's trees with a total + /// supply of zero and credits nobody. + #[test] + fn should_not_mint_base_supply_of_token_added_by_update_on_protocol_version_13() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::get(13).expect("expected protocol version 13"); + + let (contract, token_id) = + add_token_by_update(&drive, token_with_base_supply(None), platform_version); + + assert_eq!( + balance_and_total_supply( + &drive, + token_id, + contract.owner_id().to_buffer(), + platform_version + ), + (None, Some(0)) + ); + } + + #[test] + fn should_leave_token_without_base_supply_added_by_update_at_zero_supply() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let configuration = TokenConfiguration::V0( + TokenConfigurationV0::default_most_restrictive().with_base_supply(0), + ); + let (contract, token_id) = add_token_by_update(&drive, configuration, platform_version); + + assert_eq!( + balance_and_total_supply( + &drive, + token_id, + contract.owner_id().to_buffer(), + platform_version + ), + (None, Some(0)) + ); + } + + #[test] + fn should_mint_base_supply_of_every_token_added_by_one_update() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let mut contract = get_dashpay_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + contract.config_mut().set_readonly(false); + + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("insert initial contract without tokens"); + + let destination = Identifier::from(BASE_SUPPLY_DESTINATION); + contract.set_tokens(BTreeMap::from([ + (0, token_with_base_supply(None)), + (1, token_with_base_supply(Some(destination))), + ])); + contract.increment_version(); + + drive + .update_contract( + &contract, + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("update adding two tokens should succeed"); + + for (position, holder) in [(0, contract.owner_id()), (1, destination)] { + let token_id = contract + .token_id(position) + .expect("expected both tokens") + .to_buffer(); + assert_eq!( + balance_and_total_supply(&drive, token_id, holder.to_buffer(), platform_version), + (Some(BASE_SUPPLY), Some(BASE_SUPPLY)), + "token at position {position}" + ); + } + } + + /// Nothing is minted retroactively. A token added by update before + /// protocol version 14 got no base supply, and every later update finds it + /// in the original contract, so it stays that way after the upgrade. + #[test] + fn should_not_mint_base_supply_of_token_added_by_update_before_protocol_version_14() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version_13 = PlatformVersion::get(13).expect("expected protocol version 13"); + let platform_version = PlatformVersion::latest(); + + let (mut contract, token_id) = + add_token_by_update(&drive, token_with_base_supply(None), platform_version_13); + + contract.increment_version(); + drive + .update_contract( + &contract, + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("update after the upgrade should succeed"); + + assert_eq!( + balance_and_total_supply( + &drive, + token_id, + contract.owner_id().to_buffer(), + platform_version + ), + (None, Some(0)) + ); + } + + /// Neither refusal is reachable through a state transition: the update's + /// basic structure validation rejects a base supply over `i64::MAX`, and a + /// token new to a contract has no supply entry in state. + #[test] + fn should_refuse_to_mint_an_unstorable_base_supply_or_over_an_existing_total_supply() { + let token_id = [3; 32]; + + let mut operations = vec![LowLevelDriveOperation::insert_for_known_path_key_element( + total_tokens_root_supply_path_vec(), + token_id.to_vec(), + Element::new_sum_item(0), + )]; + let result = Drive::mint_base_supply_of_added_token( + token_id, + i64::MAX as u64 + 1, + BASE_SUPPLY_DESTINATION, + &mut operations, + ); + assert!(matches!( + result, + Err(Error::Protocol(error)) + if matches!(*error, ProtocolError::CriticalCorruptedCreditsCodeExecution(_)) + )); + + // `create_token_trees_operations` queues no total supply insert when + // state already holds one. + let result = Drive::mint_base_supply_of_added_token( + token_id, + BASE_SUPPLY, + BASE_SUPPLY_DESTINATION, + &mut vec![], + ); + assert!(matches!( + result, + Err(Error::Drive(DriveError::CorruptedDriveState(_))) + )); + } + + /// The base supply is minted once, by the update that adds the token. A + /// later update sees the token in the original contract and must not mint + /// again, and neither may it touch a token that got its base supply at + /// registration. + #[test] + fn should_mint_base_supply_only_in_the_update_that_adds_the_token() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let mut contract = get_dashpay_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + contract.config_mut().set_readonly(false); + contract.set_tokens(BTreeMap::from([(0, token_with_base_supply(None))])); + + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("insert initial contract with a token"); + + // The first update adds a second token next to the registered one, the + // second update changes nothing about either of them. + let mut tokens = contract.tokens().clone(); + tokens.insert(1, token_with_base_supply(None)); + contract.set_tokens(tokens); + + for _ in 0..2 { + contract.increment_version(); + drive + .update_contract( + &contract, + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("update keeping existing tokens should succeed"); + } + + for position in [0, 1] { + let token_id = contract + .token_id(position) + .expect("expected both tokens") + .to_buffer(); + assert_eq!( + balance_and_total_supply( + &drive, + token_id, + contract.owner_id().to_buffer(), + platform_version + ), + (Some(BASE_SUPPLY), Some(BASE_SUPPLY)), + "token at position {position}" + ); + } + } } diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v4.rs b/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v4.rs index eea832412b4..cf24e3132bb 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v4.rs @@ -24,6 +24,13 @@ use crate::version::drive_versions::drive_contract_method_versions::{ /// There is no backfill for tokens added by an update before this version: /// mainnet has none (checked at block 436796, where no contract update ever /// carried a token and every token's contract is still at version 1). +/// +/// The v2 contract update also mints the base supply of a token the update +/// adds, to the identity the contract insert credits at registration (the +/// token's new tokens destination identity, else the contract owner), and +/// starts the token's total supply at that amount. v1 left such a token at a +/// total supply of zero with nobody holding any of it. Nothing is minted +/// retroactively for a token added by an update before this version. pub const DRIVE_CONTRACT_METHOD_VERSIONS_V4: DriveContractMethodVersions = DriveContractMethodVersions { insert: DriveContractInsertMethodVersions { diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index 97e267545cc..534c5c9872c 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -284,6 +284,16 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// cost, and too small a token balance stays a rejection. Contracts up to /// v13 cannot carry the flag, so the waiver is inert before this version. /// +/// 13. **Tokens added by a contract update are set up like registered ones**: +/// `update_contract` v2 (`DRIVE_CONTRACT_METHOD_VERSIONS_V4`) creates the +/// perpetual, pre-programmed and once-per-identity distribution storage +/// of a token the update adds, and mints its base supply to the token's +/// `newTokensDestinationIdentity`, or to the contract owner without one, +/// with the total supply starting at the base supply. v1 did neither: a +/// claim on such a token failed as an internal error, and the token sat at +/// a total supply of zero with nobody holding any of it. Nothing is minted +/// retroactively for a token an update added under an earlier version. +/// /// * `ShieldFromIdentity` (state transition type 21) activates: /// `SHIELD_FROM_IDENTITY_INITIAL_PROTOCOL_VERSION = 14` gates it in /// `is_allowed`, and `DRIVE_ABCI_VALIDATION_VERSIONS_V10` is the first From 10e4e5895232ee54915390fb379176beadf1689c Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sat, 19 Sep 2026 20:27:25 +0700 Subject: [PATCH 2/2] docs(drive): note the base supply mint on the update_contract v2 table entry Co-Authored-By: Claude Fable 5.1 --- packages/rs-platform-version/src/version/drive_versions/v9.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rs-platform-version/src/version/drive_versions/v9.rs b/packages/rs-platform-version/src/version/drive_versions/v9.rs index 6936f540861..ddd3547f352 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v9.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v9.rs @@ -86,7 +86,7 @@ pub const DRIVE_VERSION_V9: DriveVersion = DriveVersion { }, document: DRIVE_DOCUMENT_METHOD_VERSIONS_V4, // changed in v9: v2 index walkers + v1 update walker (shared-prefix aggregate indexes become insertable) and the detect_ranked_mode slot vote: DRIVE_VOTE_METHOD_VERSIONS_V2, - contract: DRIVE_CONTRACT_METHOD_VERSIONS_V4, // changed in v9: add_contract_to_storage v1 writes the contract version item beside the contract; update_contract v2 creates the distribution storage of tokens added by an update + contract: DRIVE_CONTRACT_METHOD_VERSIONS_V4, // changed in v9: add_contract_to_storage v1 writes the contract version item beside the contract; update_contract v2 creates the distribution storage and mints the base supply of tokens added by an update fees: DriveFeesMethodVersions { calculate_fee: 0 }, estimated_costs: DriveEstimatedCostsMethodVersions { add_estimation_costs_for_levels_up_to_contract: 0,