From 425d180bd1b91c4348335d1bb583f68158009e56 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 17 Mar 2026 11:55:16 +0700 Subject: [PATCH 1/4] fix(dpp): bind SetPriceForDirectPurchase action_id to full pricing schedule The action_id for TokenSetPriceForDirectPurchaseTransition only hashed minimum_purchase_amount_and_price().1 (the credit price of the lowest tier). This meant different pricing schedules with the same minimum-tier price produced identical action_ids, enabling the same vote-swap attack as the token config update vulnerability fixed in PR #3346. Examples of collisions in the old code: - SetPrices({1: 100, 10: 800}) vs SetPrices({1: 100, 10: 9999}) - SinglePrice(100) vs SetPrices({1: 100}) - SetPrices({5: 100, ...}) vs SetPrices({10: 100, ...}) Fix: serialize the full TokenPricingSchedule with bincode and include it in the hash. This binds the voted-on pricing schedule into the action_id, preventing vote-swap attacks. Note: Unlike the config update fix, this is NOT gated behind a platform version because SetPriceForDirectPurchase is a new feature (v3.1) that has not been used in production yet. There is no backward compatibility concern. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../v0_methods.rs | 118 +++++++++++++++++- 1 file changed, 112 insertions(+), 6 deletions(-) diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0_methods.rs index dc838ac1e78..6ed7630089e 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0_methods.rs @@ -79,6 +79,12 @@ impl AllowedAsMultiPartyAction for TokenSetPriceForDirectPurchaseTransition { } impl TokenSetPriceForDirectPurchaseTransition { + /// Computes the action_id by hashing the full serialized pricing schedule. + /// + /// Previous versions only hashed the minimum-tier credit price, which meant + /// different pricing schedules with the same minimum price (e.g., + /// `SetPrices({1: 100, 10: 800})` vs `SetPrices({1: 100, 10: 9999})`) + /// produced identical action_ids, enabling vote-swap attacks. pub fn calculate_action_id_with_fields( token_id: &[u8; 32], owner_id: &[u8; 32], @@ -90,14 +96,114 @@ impl TokenSetPriceForDirectPurchaseTransition { bytes.extend_from_slice(owner_id); bytes.extend_from_slice(&identity_contract_nonce.to_be_bytes()); if let Some(price_per_token) = price_per_token { - bytes.extend_from_slice( - &price_per_token - .minimum_purchase_amount_and_price() - .1 - .to_be_bytes(), - ); + let serialized = + bincode::encode_to_vec(price_per_token, bincode::config::standard()) + .expect("expected to encode pricing schedule"); + bytes.extend_from_slice(&serialized); } hash_double(bytes).into() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::token_set_price_for_direct_purchase_transition::TokenSetPriceForDirectPurchaseTransitionV0; + use std::collections::BTreeMap; + + fn make_transition( + price: Option, + ) -> TokenSetPriceForDirectPurchaseTransition { + TokenSetPriceForDirectPurchaseTransition::V0( + TokenSetPriceForDirectPurchaseTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::new([1u8; 32]), + token_id: Identifier::new([2u8; 32]), + using_group_info: None, + }), + price, + public_note: None, + }, + ) + } + + #[test] + fn different_set_prices_with_same_minimum_produce_different_ids() { + // This was the vulnerability: two SetPrices schedules with the same + // minimum-tier price but different higher tiers produced identical + // action_ids when only minimum_purchase_amount_and_price().1 was hashed. + let owner_id = Identifier::new([3u8; 32]); + + let t_cheap = make_transition(Some(TokenPricingSchedule::SetPrices( + BTreeMap::from([(1, 100), (10, 800)]), + ))); + let t_expensive = make_transition(Some(TokenPricingSchedule::SetPrices( + BTreeMap::from([(1, 100), (10, 9999)]), + ))); + + let id_cheap = t_cheap.calculate_action_id(owner_id); + let id_expensive = t_expensive.calculate_action_id(owner_id); + + assert_ne!( + id_cheap, id_expensive, + "different pricing schedules with same minimum price must produce different action_ids" + ); + } + + #[test] + fn single_price_and_set_prices_with_same_minimum_produce_different_ids() { + // SinglePrice(100) and SetPrices({1: 100}) both have + // minimum_purchase_amount_and_price() == (1, 100), but they are + // semantically different schedules. + let owner_id = Identifier::new([3u8; 32]); + + let t_single = make_transition(Some(TokenPricingSchedule::SinglePrice(100))); + let t_set = make_transition(Some(TokenPricingSchedule::SetPrices( + BTreeMap::from([(1, 100)]), + ))); + + let id_single = t_single.calculate_action_id(owner_id); + let id_set = t_set.calculate_action_id(owner_id); + + assert_ne!( + id_single, id_set, + "SinglePrice and SetPrices with same minimum must produce different action_ids" + ); + } + + #[test] + fn identical_schedules_produce_same_id() { + let owner_id = Identifier::new([3u8; 32]); + + let t1 = make_transition(Some(TokenPricingSchedule::SetPrices( + BTreeMap::from([(1, 100), (10, 800)]), + ))); + let t2 = make_transition(Some(TokenPricingSchedule::SetPrices( + BTreeMap::from([(1, 100), (10, 800)]), + ))); + + assert_eq!( + t1.calculate_action_id(owner_id), + t2.calculate_action_id(owner_id), + "identical pricing schedules must produce the same action_id" + ); + } + + #[test] + fn none_price_produces_different_id_from_some_price() { + let owner_id = Identifier::new([3u8; 32]); + + let t_none = make_transition(None); + let t_some = make_transition(Some(TokenPricingSchedule::SinglePrice(100))); + + assert_ne!( + t_none.calculate_action_id(owner_id), + t_some.calculate_action_id(owner_id), + "None price and Some price must produce different action_ids" + ); + } +} From b64180b52aa1cbc1809c4db8c694e33e90c50243 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 17 Mar 2026 12:15:22 +0700 Subject: [PATCH 2/4] fix: use Result instead of expect, fix tests for updated trait signature - calculate_action_id_with_fields returns Result with map_err instead of using expect for bincode encoding - Update wrapper and v0 impls to match the AllowedAsMultiPartyAction trait signature from #3346 (platform_version + Result) - Fix all tests to pass platform_version and unwrap Results - Rebase on #3346 branch Co-Authored-By: Claude Opus 4.6 (1M context) --- .../v0/v0_methods.rs | 12 +- .../v0_methods.rs | 110 ++++++++++-------- 2 files changed, 69 insertions(+), 53 deletions(-) diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0/v0_methods.rs index b987d4664e2..efb333845bb 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0/v0_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0/v0_methods.rs @@ -72,13 +72,11 @@ impl AllowedAsMultiPartyAction for TokenSetPriceForDirectPurchaseTransitionV0 { ) -> Result { let TokenSetPriceForDirectPurchaseTransitionV0 { base, price, .. } = self; - Ok( - TokenSetPriceForDirectPurchaseTransition::calculate_action_id_with_fields( - base.token_id().as_bytes(), - owner_id.as_bytes(), - base.identity_contract_nonce(), - price.as_ref(), - ), + TokenSetPriceForDirectPurchaseTransition::calculate_action_id_with_fields( + base.token_id().as_bytes(), + owner_id.as_bytes(), + base.identity_contract_nonce(), + price.as_ref(), ) } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0_methods.rs index 6ed7630089e..2b2a31d80e0 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0_methods.rs @@ -1,14 +1,14 @@ -use platform_value::Identifier; -use platform_version::version::PlatformVersion; +use crate::errors::ProtocolError; use crate::prelude::IdentityNonce; -use crate::ProtocolError; use crate::state_transition::batch_transition::batched_transition::multi_party_action::AllowedAsMultiPartyAction; use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; -use crate::state_transition::batch_transition::token_set_price_for_direct_purchase_transition::TokenSetPriceForDirectPurchaseTransition; use crate::state_transition::batch_transition::token_set_price_for_direct_purchase_transition::v0::v0_methods::TokenSetPriceForDirectPurchaseTransitionV0Methods; +use crate::state_transition::batch_transition::token_set_price_for_direct_purchase_transition::TokenSetPriceForDirectPurchaseTransition; use crate::tokens::token_pricing_schedule::TokenPricingSchedule; use crate::util::hash::hash_double; +use platform_value::Identifier; +use platform_version::version::PlatformVersion; impl TokenBaseTransitionAccessors for TokenSetPriceForDirectPurchaseTransition { fn base(&self) -> &TokenBaseTransition { @@ -90,19 +90,18 @@ impl TokenSetPriceForDirectPurchaseTransition { owner_id: &[u8; 32], identity_contract_nonce: IdentityNonce, price_per_token: Option<&TokenPricingSchedule>, - ) -> Identifier { + ) -> Result { let mut bytes = b"action_token_set_price_for_direct_purchase".to_vec(); bytes.extend_from_slice(token_id); bytes.extend_from_slice(owner_id); bytes.extend_from_slice(&identity_contract_nonce.to_be_bytes()); if let Some(price_per_token) = price_per_token { - let serialized = - bincode::encode_to_vec(price_per_token, bincode::config::standard()) - .expect("expected to encode pricing schedule"); + let serialized = bincode::encode_to_vec(price_per_token, bincode::config::standard()) + .map_err(|e| ProtocolError::EncodingError(e.to_string()))?; bytes.extend_from_slice(&serialized); } - hash_double(bytes).into() + Ok(hash_double(bytes).into()) } } @@ -111,24 +110,23 @@ mod tests { use super::*; use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; use crate::state_transition::batch_transition::token_set_price_for_direct_purchase_transition::TokenSetPriceForDirectPurchaseTransitionV0; + use platform_version::version::PlatformVersion; use std::collections::BTreeMap; fn make_transition( price: Option, ) -> TokenSetPriceForDirectPurchaseTransition { - TokenSetPriceForDirectPurchaseTransition::V0( - TokenSetPriceForDirectPurchaseTransitionV0 { - base: TokenBaseTransition::V0(TokenBaseTransitionV0 { - identity_contract_nonce: 1, - token_contract_position: 0, - data_contract_id: Identifier::new([1u8; 32]), - token_id: Identifier::new([2u8; 32]), - using_group_info: None, - }), - price, - public_note: None, - }, - ) + TokenSetPriceForDirectPurchaseTransition::V0(TokenSetPriceForDirectPurchaseTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::new([1u8; 32]), + token_id: Identifier::new([2u8; 32]), + using_group_info: None, + }), + price, + public_note: None, + }) } #[test] @@ -138,15 +136,21 @@ mod tests { // action_ids when only minimum_purchase_amount_and_price().1 was hashed. let owner_id = Identifier::new([3u8; 32]); - let t_cheap = make_transition(Some(TokenPricingSchedule::SetPrices( - BTreeMap::from([(1, 100), (10, 800)]), - ))); - let t_expensive = make_transition(Some(TokenPricingSchedule::SetPrices( - BTreeMap::from([(1, 100), (10, 9999)]), - ))); - - let id_cheap = t_cheap.calculate_action_id(owner_id); - let id_expensive = t_expensive.calculate_action_id(owner_id); + let t_cheap = make_transition(Some(TokenPricingSchedule::SetPrices(BTreeMap::from([ + (1, 100), + (10, 800), + ])))); + let t_expensive = make_transition(Some(TokenPricingSchedule::SetPrices(BTreeMap::from([ + (1, 100), + (10, 9999), + ])))); + + let id_cheap = t_cheap + .calculate_action_id(owner_id, PlatformVersion::latest()) + .expect("expected action id"); + let id_expensive = t_expensive + .calculate_action_id(owner_id, PlatformVersion::latest()) + .expect("expected action id"); assert_ne!( id_cheap, id_expensive, @@ -162,12 +166,16 @@ mod tests { let owner_id = Identifier::new([3u8; 32]); let t_single = make_transition(Some(TokenPricingSchedule::SinglePrice(100))); - let t_set = make_transition(Some(TokenPricingSchedule::SetPrices( - BTreeMap::from([(1, 100)]), - ))); + let t_set = make_transition(Some(TokenPricingSchedule::SetPrices(BTreeMap::from([( + 1, 100, + )])))); - let id_single = t_single.calculate_action_id(owner_id); - let id_set = t_set.calculate_action_id(owner_id); + let id_single = t_single + .calculate_action_id(owner_id, PlatformVersion::latest()) + .expect("expected action id"); + let id_set = t_set + .calculate_action_id(owner_id, PlatformVersion::latest()) + .expect("expected action id"); assert_ne!( id_single, id_set, @@ -179,16 +187,21 @@ mod tests { fn identical_schedules_produce_same_id() { let owner_id = Identifier::new([3u8; 32]); - let t1 = make_transition(Some(TokenPricingSchedule::SetPrices( - BTreeMap::from([(1, 100), (10, 800)]), - ))); - let t2 = make_transition(Some(TokenPricingSchedule::SetPrices( - BTreeMap::from([(1, 100), (10, 800)]), - ))); + let t1 = make_transition(Some(TokenPricingSchedule::SetPrices(BTreeMap::from([ + (1, 100), + (10, 800), + ])))); + let t2 = make_transition(Some(TokenPricingSchedule::SetPrices(BTreeMap::from([ + (1, 100), + (10, 800), + ])))); + let pv = PlatformVersion::latest(); assert_eq!( - t1.calculate_action_id(owner_id), - t2.calculate_action_id(owner_id), + t1.calculate_action_id(owner_id, pv) + .expect("expected action id"), + t2.calculate_action_id(owner_id, pv) + .expect("expected action id"), "identical pricing schedules must produce the same action_id" ); } @@ -196,13 +209,18 @@ mod tests { #[test] fn none_price_produces_different_id_from_some_price() { let owner_id = Identifier::new([3u8; 32]); + let pv = PlatformVersion::latest(); let t_none = make_transition(None); let t_some = make_transition(Some(TokenPricingSchedule::SinglePrice(100))); assert_ne!( - t_none.calculate_action_id(owner_id), - t_some.calculate_action_id(owner_id), + t_none + .calculate_action_id(owner_id, pv) + .expect("expected action id"), + t_some + .calculate_action_id(owner_id, pv) + .expect("expected action id"), "None price and Some price must produce different action_ids" ); } From ff10b66a1564ce33475b2ca2a4eba4fbe54c309f Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 17 Mar 2026 13:10:22 +0700 Subject: [PATCH 3/4] fix(dpp): version-gate SetPriceForDirectPurchase action_id calculation Gate the action_id fix behind platform version, matching the pattern from #3346 (token config update): - Add token_set_price_action_id_version field to DPPTokenVersions - v0: hashes only minimum_purchase_amount_and_price().1 (existing) - v1: hashes the full serialized TokenPricingSchedule (fix) - Create TOKEN_VERSIONS_V3 with both fixes enabled - Update v12 to use TOKEN_VERSIONS_V3 - Add v0 vulnerability regression test and v0-vs-v1 differentiation test Co-Authored-By: Claude Opus 4.6 (1M context) --- .../v0/v0_methods.rs | 3 +- .../v0_methods.rs | 114 +++++++++++++++++- .../dpp_versions/dpp_token_versions/mod.rs | 5 + .../dpp_versions/dpp_token_versions/v1.rs | 1 + .../dpp_versions/dpp_token_versions/v2.rs | 1 + .../dpp_versions/dpp_token_versions/v3.rs | 9 ++ .../rs-platform-version/src/version/v12.rs | 4 +- 7 files changed, 128 insertions(+), 9 deletions(-) create mode 100644 packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v3.rs diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0/v0_methods.rs index efb333845bb..026886db616 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0/v0_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0/v0_methods.rs @@ -68,7 +68,7 @@ impl AllowedAsMultiPartyAction for TokenSetPriceForDirectPurchaseTransitionV0 { fn calculate_action_id( &self, owner_id: Identifier, - _platform_version: &PlatformVersion, + platform_version: &PlatformVersion, ) -> Result { let TokenSetPriceForDirectPurchaseTransitionV0 { base, price, .. } = self; @@ -77,6 +77,7 @@ impl AllowedAsMultiPartyAction for TokenSetPriceForDirectPurchaseTransitionV0 { owner_id.as_bytes(), base.identity_contract_nonce(), price.as_ref(), + platform_version, ) } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0_methods.rs index 2b2a31d80e0..5d9feec15cb 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0_methods.rs @@ -79,17 +79,67 @@ impl AllowedAsMultiPartyAction for TokenSetPriceForDirectPurchaseTransition { } impl TokenSetPriceForDirectPurchaseTransition { - /// Computes the action_id by hashing the full serialized pricing schedule. - /// - /// Previous versions only hashed the minimum-tier credit price, which meant - /// different pricing schedules with the same minimum price (e.g., - /// `SetPrices({1: 100, 10: 800})` vs `SetPrices({1: 100, 10: 9999})`) - /// produced identical action_ids, enabling vote-swap attacks. pub fn calculate_action_id_with_fields( token_id: &[u8; 32], owner_id: &[u8; 32], identity_contract_nonce: IdentityNonce, price_per_token: Option<&TokenPricingSchedule>, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .dpp + .token_versions + .token_set_price_action_id_version + { + 0 => Ok(Self::calculate_action_id_with_fields_v0( + token_id, + owner_id, + identity_contract_nonce, + price_per_token, + )), + 1 => Self::calculate_action_id_with_fields_v1( + token_id, + owner_id, + identity_contract_nonce, + price_per_token, + ), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "calculate_action_id_with_fields".to_string(), + known_versions: vec![0, 1], + received: version, + }), + } + } + + /// v0: hashes only minimum_purchase_amount_and_price().1 (kept for backward compat). + fn calculate_action_id_with_fields_v0( + token_id: &[u8; 32], + owner_id: &[u8; 32], + identity_contract_nonce: IdentityNonce, + price_per_token: Option<&TokenPricingSchedule>, + ) -> Identifier { + let mut bytes = b"action_token_set_price_for_direct_purchase".to_vec(); + bytes.extend_from_slice(token_id); + bytes.extend_from_slice(owner_id); + bytes.extend_from_slice(&identity_contract_nonce.to_be_bytes()); + if let Some(price_per_token) = price_per_token { + bytes.extend_from_slice( + &price_per_token + .minimum_purchase_amount_and_price() + .1 + .to_be_bytes(), + ); + } + + hash_double(bytes).into() + } + + /// v1: hashes the full serialized TokenPricingSchedule, preventing schedule swap. + fn calculate_action_id_with_fields_v1( + token_id: &[u8; 32], + owner_id: &[u8; 32], + identity_contract_nonce: IdentityNonce, + price_per_token: Option<&TokenPricingSchedule>, ) -> Result { let mut bytes = b"action_token_set_price_for_direct_purchase".to_vec(); bytes.extend_from_slice(token_id); @@ -224,4 +274,56 @@ mod tests { "None price and Some price must produce different action_ids" ); } + + #[test] + fn v0_same_minimum_produces_same_id_vulnerability() { + // Documents the v0 vulnerability: different schedules with the same + // minimum-tier price produce identical action_ids. + let owner_id = Identifier::new([3u8; 32]); + let pv = PlatformVersion::first(); + + let t_cheap = make_transition(Some(TokenPricingSchedule::SetPrices(BTreeMap::from([ + (1, 100), + (10, 800), + ])))); + let t_expensive = make_transition(Some(TokenPricingSchedule::SetPrices(BTreeMap::from([ + (1, 100), + (10, 9999), + ])))); + + let id_cheap = t_cheap + .calculate_action_id(owner_id, pv) + .expect("expected action id"); + let id_expensive = t_expensive + .calculate_action_id(owner_id, pv) + .expect("expected action id"); + + // v0: these are EQUAL -- the vulnerability + assert_eq!( + id_cheap, id_expensive, + "v0 should produce the same action_id for different schedules with same min price" + ); + } + + #[test] + fn v0_and_v1_produce_different_ids_for_same_input() { + let owner_id = Identifier::new([3u8; 32]); + + let t = make_transition(Some(TokenPricingSchedule::SetPrices(BTreeMap::from([ + (1, 100), + (10, 800), + ])))); + + let id_v0 = t + .calculate_action_id(owner_id, PlatformVersion::first()) + .expect("expected action id"); + let id_v1 = t + .calculate_action_id(owner_id, PlatformVersion::latest()) + .expect("expected action id"); + + assert_ne!( + id_v0, id_v1, + "v0 and v1 should produce different action_ids for the same schedule" + ); + } } diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/mod.rs index ed2283c859a..589fa8c6268 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/mod.rs @@ -1,5 +1,6 @@ pub mod v1; pub mod v2; +pub mod v3; use versioned_feature_core::FeatureVersion; @@ -12,4 +13,8 @@ pub struct DPPTokenVersions { /// v0: uses only the u8 discriminant of the config change item (vulnerable to value swap) /// v1: includes the full serialized config change item in the hash pub token_config_update_action_id_version: FeatureVersion, + /// Version for the set-price-for-direct-purchase action_id calculation. + /// v0: uses only minimum_purchase_amount_and_price().1 (vulnerable to schedule swap) + /// v1: includes the full serialized TokenPricingSchedule in the hash + pub token_set_price_action_id_version: FeatureVersion, } diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v1.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v1.rs index 4e3e8e3861a..e5114478c72 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v1.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v1.rs @@ -5,4 +5,5 @@ pub const TOKEN_VERSIONS_V1: DPPTokenVersions = DPPTokenVersions { identity_token_status_default_structure_version: 0, token_contract_info_default_structure_version: 0, token_config_update_action_id_version: 0, + token_set_price_action_id_version: 0, }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v2.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v2.rs index 32c2c0fdfa1..fccbc9e4bcb 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v2.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v2.rs @@ -5,4 +5,5 @@ pub const TOKEN_VERSIONS_V2: DPPTokenVersions = DPPTokenVersions { identity_token_status_default_structure_version: 0, token_contract_info_default_structure_version: 0, token_config_update_action_id_version: 1, + token_set_price_action_id_version: 0, }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v3.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v3.rs new file mode 100644 index 00000000000..18b26b9734c --- /dev/null +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v3.rs @@ -0,0 +1,9 @@ +use crate::version::dpp_versions::dpp_token_versions::DPPTokenVersions; + +pub const TOKEN_VERSIONS_V3: DPPTokenVersions = DPPTokenVersions { + identity_token_info_default_structure_version: 0, + identity_token_status_default_structure_version: 0, + token_contract_info_default_structure_version: 0, + token_config_update_action_id_version: 1, + token_set_price_action_id_version: 1, +}; diff --git a/packages/rs-platform-version/src/version/v12.rs b/packages/rs-platform-version/src/version/v12.rs index 394260d2b49..319ed827936 100644 --- a/packages/rs-platform-version/src/version/v12.rs +++ b/packages/rs-platform-version/src/version/v12.rs @@ -10,7 +10,7 @@ use crate::version::dpp_versions::dpp_state_transition_conversion_versions::v2:: use crate::version::dpp_versions::dpp_state_transition_method_versions::v1::STATE_TRANSITION_METHOD_VERSIONS_V1; use crate::version::dpp_versions::dpp_state_transition_serialization_versions::v2::STATE_TRANSITION_SERIALIZATION_VERSIONS_V2; use crate::version::dpp_versions::dpp_state_transition_versions::v3::STATE_TRANSITION_VERSIONS_V3; -use crate::version::dpp_versions::dpp_token_versions::v2::TOKEN_VERSIONS_V2; +use crate::version::dpp_versions::dpp_token_versions::v3::TOKEN_VERSIONS_V3; use crate::version::dpp_versions::dpp_validation_versions::v3::DPP_VALIDATION_VERSIONS_V3; use crate::version::dpp_versions::dpp_voting_versions::v2::VOTING_VERSION_V2; use crate::version::dpp_versions::DPPVersion; @@ -55,7 +55,7 @@ pub const PLATFORM_V12: PlatformVersion = PlatformVersion { document_versions: DOCUMENT_VERSIONS_V3, identity_versions: IDENTITY_VERSIONS_V1, voting_versions: VOTING_VERSION_V2, - token_versions: TOKEN_VERSIONS_V2, // fixes issue with token config update action id + token_versions: TOKEN_VERSIONS_V3, // fixes action_id vote-swap for config update + set price asset_lock_versions: DPP_ASSET_LOCK_VERSIONS_V1, methods: DPP_METHOD_VERSIONS_V2, factory_versions: DPP_FACTORY_VERSIONS_V1, From 48447ca7bcfe6dc7e9a7270cb74b9001f779c15f Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 17 Mar 2026 13:13:24 +0700 Subject: [PATCH 4/4] refactor: fold set_price fix into TOKEN_VERSIONS_V2 (not yet activated) TOKEN_VERSIONS_V2 hasn't activated in production yet, so there's no need for a separate V3. Set token_set_price_action_id_version: 1 directly in V2 alongside token_config_update_action_id_version: 1. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/version/dpp_versions/dpp_token_versions/mod.rs | 1 - .../src/version/dpp_versions/dpp_token_versions/v2.rs | 2 +- .../src/version/dpp_versions/dpp_token_versions/v3.rs | 9 --------- packages/rs-platform-version/src/version/v12.rs | 4 ++-- 4 files changed, 3 insertions(+), 13 deletions(-) delete mode 100644 packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v3.rs diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/mod.rs index 589fa8c6268..ede323f1d33 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/mod.rs @@ -1,6 +1,5 @@ pub mod v1; pub mod v2; -pub mod v3; use versioned_feature_core::FeatureVersion; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v2.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v2.rs index fccbc9e4bcb..c9f0cc893e0 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v2.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v2.rs @@ -5,5 +5,5 @@ pub const TOKEN_VERSIONS_V2: DPPTokenVersions = DPPTokenVersions { identity_token_status_default_structure_version: 0, token_contract_info_default_structure_version: 0, token_config_update_action_id_version: 1, - token_set_price_action_id_version: 0, + token_set_price_action_id_version: 1, }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v3.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v3.rs deleted file mode 100644 index 18b26b9734c..00000000000 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v3.rs +++ /dev/null @@ -1,9 +0,0 @@ -use crate::version::dpp_versions::dpp_token_versions::DPPTokenVersions; - -pub const TOKEN_VERSIONS_V3: DPPTokenVersions = DPPTokenVersions { - identity_token_info_default_structure_version: 0, - identity_token_status_default_structure_version: 0, - token_contract_info_default_structure_version: 0, - token_config_update_action_id_version: 1, - token_set_price_action_id_version: 1, -}; diff --git a/packages/rs-platform-version/src/version/v12.rs b/packages/rs-platform-version/src/version/v12.rs index 319ed827936..fbe019d3afd 100644 --- a/packages/rs-platform-version/src/version/v12.rs +++ b/packages/rs-platform-version/src/version/v12.rs @@ -10,7 +10,7 @@ use crate::version::dpp_versions::dpp_state_transition_conversion_versions::v2:: use crate::version::dpp_versions::dpp_state_transition_method_versions::v1::STATE_TRANSITION_METHOD_VERSIONS_V1; use crate::version::dpp_versions::dpp_state_transition_serialization_versions::v2::STATE_TRANSITION_SERIALIZATION_VERSIONS_V2; use crate::version::dpp_versions::dpp_state_transition_versions::v3::STATE_TRANSITION_VERSIONS_V3; -use crate::version::dpp_versions::dpp_token_versions::v3::TOKEN_VERSIONS_V3; +use crate::version::dpp_versions::dpp_token_versions::v2::TOKEN_VERSIONS_V2; use crate::version::dpp_versions::dpp_validation_versions::v3::DPP_VALIDATION_VERSIONS_V3; use crate::version::dpp_versions::dpp_voting_versions::v2::VOTING_VERSION_V2; use crate::version::dpp_versions::DPPVersion; @@ -55,7 +55,7 @@ pub const PLATFORM_V12: PlatformVersion = PlatformVersion { document_versions: DOCUMENT_VERSIONS_V3, identity_versions: IDENTITY_VERSIONS_V1, voting_versions: VOTING_VERSION_V2, - token_versions: TOKEN_VERSIONS_V3, // fixes action_id vote-swap for config update + set price + token_versions: TOKEN_VERSIONS_V2, // fixes action_id vote-swap for config update + set price asset_lock_versions: DPP_ASSET_LOCK_VERSIONS_V1, methods: DPP_METHOD_VERSIONS_V2, factory_versions: DPP_FACTORY_VERSIONS_V1,