diff --git a/CHANGELOG.md b/CHANGELOG.md index 3da090fd89..0b6c1e9a05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,19 @@ and this project adheres to ## [Unreleased] +## Fixed + +- cosmwasm-std: Fix deserialization of `DenomMetadata`. ([#2417]) +- cosmwasm-std: Deprecate `PayPacketFee`, `PayPacketFeeAsync`, `IbcFee`. IBC + fees have been removed from ibc-go in version 10. The mentioned struct and + enum fields are deprecated and will be removed in cosmwasm `3.0` ([#2431]) +- cosmwasm-std: Deprecate `FeeEnabledChannel` and `FeeEnabledChannelResponse` + ([#2481]) + +[#2417]: https://github.com/CosmWasm/cosmwasm/pull/2417 +[#2431]: https://github.com/CosmWasm/cosmwasm/pull/2431 +[#2481]: https://github.com/CosmWasm/cosmwasm/pull/2481 + ## [2.2.2] - 2025-03-05 ### Changed @@ -17,6 +30,10 @@ and this project adheres to [#2384]: https://github.com/CosmWasm/cosmwasm/pull/2384 +## Fixed + +- cosmwasm-vm: Fix CWA-2025-003. + ## [2.2.1] - 2025-02-04 ## Added diff --git a/contracts/ibc-reflect/schema/ibc/packet_msg.json b/contracts/ibc-reflect/schema/ibc/packet_msg.json index eec0371bd4..437bb46ac6 100644 --- a/contracts/ibc-reflect/schema/ibc/packet_msg.json +++ b/contracts/ibc-reflect/schema/ibc/packet_msg.json @@ -416,6 +416,7 @@ "additionalProperties": false }, "IbcFee": { + "deprecated": true, "type": "object", "required": [ "ack_fee", @@ -598,6 +599,7 @@ }, { "description": "Incentivizes the next IBC packet sent after this message with a fee. Note that this does not necessarily have to be a packet sent by this contract. The fees are taken from the contract's balance immediately and locked until the packet is handled.\n\n# Example\n\nMost commonly, you will attach this message to a response right before sending a packet using [`IbcMsg::SendPacket`] or [`IbcMsg::Transfer`].\n\n```rust # use cosmwasm_std::{IbcMsg, IbcEndpoint, IbcFee, IbcTimeout, Coin, coins, CosmosMsg, Response, Timestamp};\n\nlet incentivize = IbcMsg::PayPacketFee { port_id: \"transfer\".to_string(), channel_id: \"source-channel\".to_string(), fee: IbcFee { receive_fee: coins(100, \"token\"), ack_fee: coins(201, \"token\"), timeout_fee: coins(200, \"token\"), }, relayers: vec![], }; let transfer = IbcMsg::Transfer { channel_id: \"source-channel\".to_string(), to_address: \"receiver\".to_string(), amount: Coin::new(100u32, \"token\"), timeout: IbcTimeout::with_timestamp(Timestamp::from_nanos(0)), memo: None, };\n\n# #[cfg(feature = \"stargate\")] let _: Response = Response::new() .add_message(CosmosMsg::Ibc(incentivize)) .add_message(CosmosMsg::Ibc(transfer)); ```", + "deprecated": true, "type": "object", "required": [ "pay_packet_fee" @@ -638,6 +640,7 @@ }, { "description": "Incentivizes the existing IBC packet with the given port, channel and sequence with a fee. Note that this does not necessarily have to be a packet sent by this contract. The fees are taken from the contract's balance immediately and locked until the packet is handled. They are added to the existing fees on the packet.", + "deprecated": true, "type": "object", "required": [ "pay_packet_fee_async" diff --git a/contracts/reflect/schema/raw/execute.json b/contracts/reflect/schema/raw/execute.json index 60689b4e44..869f13e077 100644 --- a/contracts/reflect/schema/raw/execute.json +++ b/contracts/reflect/schema/raw/execute.json @@ -489,6 +489,7 @@ "additionalProperties": false }, "IbcFee": { + "deprecated": true, "type": "object", "required": [ "ack_fee", @@ -671,6 +672,7 @@ }, { "description": "Incentivizes the next IBC packet sent after this message with a fee. Note that this does not necessarily have to be a packet sent by this contract. The fees are taken from the contract's balance immediately and locked until the packet is handled.\n\n# Example\n\nMost commonly, you will attach this message to a response right before sending a packet using [`IbcMsg::SendPacket`] or [`IbcMsg::Transfer`].\n\n```rust # use cosmwasm_std::{IbcMsg, IbcEndpoint, IbcFee, IbcTimeout, Coin, coins, CosmosMsg, Response, Timestamp};\n\nlet incentivize = IbcMsg::PayPacketFee { port_id: \"transfer\".to_string(), channel_id: \"source-channel\".to_string(), fee: IbcFee { receive_fee: coins(100, \"token\"), ack_fee: coins(201, \"token\"), timeout_fee: coins(200, \"token\"), }, relayers: vec![], }; let transfer = IbcMsg::Transfer { channel_id: \"source-channel\".to_string(), to_address: \"receiver\".to_string(), amount: Coin::new(100u32, \"token\"), timeout: IbcTimeout::with_timestamp(Timestamp::from_nanos(0)), memo: None, };\n\n# #[cfg(feature = \"stargate\")] let _: Response = Response::new() .add_message(CosmosMsg::Ibc(incentivize)) .add_message(CosmosMsg::Ibc(transfer)); ```", + "deprecated": true, "type": "object", "required": [ "pay_packet_fee" @@ -711,6 +713,7 @@ }, { "description": "Incentivizes the existing IBC packet with the given port, channel and sequence with a fee. Note that this does not necessarily have to be a packet sent by this contract. The fees are taken from the contract's balance immediately and locked until the packet is handled. They are added to the existing fees on the packet.", + "deprecated": true, "type": "object", "required": [ "pay_packet_fee_async" diff --git a/contracts/reflect/schema/raw/query.json b/contracts/reflect/schema/raw/query.json index 2419726e55..ff1d9d5a98 100644 --- a/contracts/reflect/schema/raw/query.json +++ b/contracts/reflect/schema/raw/query.json @@ -427,6 +427,7 @@ }, { "description": "Queries whether the given channel supports IBC fees. If port_id is omitted, it will default to the contract's own channel. (To save a PortId{} call)\n\nReturns a `FeeEnabledChannelResponse`.", + "deprecated": true, "type": "object", "required": [ "fee_enabled_channel" diff --git a/contracts/reflect/schema/reflect.json b/contracts/reflect/schema/reflect.json index 7d59934563..382771e0c0 100644 --- a/contracts/reflect/schema/reflect.json +++ b/contracts/reflect/schema/reflect.json @@ -499,6 +499,7 @@ "additionalProperties": false }, "IbcFee": { + "deprecated": true, "type": "object", "required": [ "ack_fee", @@ -681,6 +682,7 @@ }, { "description": "Incentivizes the next IBC packet sent after this message with a fee. Note that this does not necessarily have to be a packet sent by this contract. The fees are taken from the contract's balance immediately and locked until the packet is handled.\n\n# Example\n\nMost commonly, you will attach this message to a response right before sending a packet using [`IbcMsg::SendPacket`] or [`IbcMsg::Transfer`].\n\n```rust # use cosmwasm_std::{IbcMsg, IbcEndpoint, IbcFee, IbcTimeout, Coin, coins, CosmosMsg, Response, Timestamp};\n\nlet incentivize = IbcMsg::PayPacketFee { port_id: \"transfer\".to_string(), channel_id: \"source-channel\".to_string(), fee: IbcFee { receive_fee: coins(100, \"token\"), ack_fee: coins(201, \"token\"), timeout_fee: coins(200, \"token\"), }, relayers: vec![], }; let transfer = IbcMsg::Transfer { channel_id: \"source-channel\".to_string(), to_address: \"receiver\".to_string(), amount: Coin::new(100u32, \"token\"), timeout: IbcTimeout::with_timestamp(Timestamp::from_nanos(0)), memo: None, };\n\n# #[cfg(feature = \"stargate\")] let _: Response = Response::new() .add_message(CosmosMsg::Ibc(incentivize)) .add_message(CosmosMsg::Ibc(transfer)); ```", + "deprecated": true, "type": "object", "required": [ "pay_packet_fee" @@ -721,6 +723,7 @@ }, { "description": "Incentivizes the existing IBC packet with the given port, channel and sequence with a fee. Note that this does not necessarily have to be a packet sent by this contract. The fees are taken from the contract's balance immediately and locked until the packet is handled. They are added to the existing fees on the packet.", + "deprecated": true, "type": "object", "required": [ "pay_packet_fee_async" @@ -1687,6 +1690,7 @@ }, { "description": "Queries whether the given channel supports IBC fees. If port_id is omitted, it will default to the contract's own channel. (To save a PortId{} call)\n\nReturns a `FeeEnabledChannelResponse`.", + "deprecated": true, "type": "object", "required": [ "fee_enabled_channel" diff --git a/packages/std/src/ibc.rs b/packages/std/src/ibc.rs index 5612cf5ebb..fa3260122a 100644 --- a/packages/std/src/ibc.rs +++ b/packages/std/src/ibc.rs @@ -1,3 +1,5 @@ +#![allow(deprecated)] + // The CosmosMsg variants are defined in results/cosmos_msg.rs // The rest of the IBC related functionality is defined here @@ -111,6 +113,10 @@ pub enum IbcMsg { /// .add_message(CosmosMsg::Ibc(transfer)); /// ``` #[cfg(feature = "cosmwasm_2_2")] + #[deprecated( + since = "2.2.3", + note = "IBC fees have been removed from ibc-go `v10`, which is used in wasmd `v0.55.0`." + )] PayPacketFee { /// The port id on the chain where the packet is sent from (this chain). port_id: String, @@ -128,6 +134,10 @@ pub enum IbcMsg { /// The fees are taken from the contract's balance immediately and locked until the packet is handled. /// They are added to the existing fees on the packet. #[cfg(feature = "cosmwasm_2_2")] + #[deprecated( + since = "2.2.3", + note = "IBC fees have been removed from ibc-go `v10`, which is used in wasmd `v0.55.0`." + )] PayPacketFeeAsync { /// The port id on the chain where the packet is sent from (this chain). port_id: String, @@ -144,6 +154,10 @@ pub enum IbcMsg { }, } +#[deprecated( + since = "2.2.3", + note = "IBC fees have been removed from ibc-go `v10`, which is used in wasmd `v0.55.0`." +)] #[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq, JsonSchema)] pub struct IbcFee { // the packet receive fee diff --git a/packages/std/src/lib.rs b/packages/std/src/lib.rs index eede5242a0..39fd88d9e4 100644 --- a/packages/std/src/lib.rs +++ b/packages/std/src/lib.rs @@ -67,10 +67,12 @@ pub use crate::errors::{ }; pub use crate::hex_binary::HexBinary; pub use crate::ibc::IbcChannelOpenResponse; +#[allow(deprecated)] +pub use crate::ibc::IbcFee; pub use crate::ibc::{ Ibc3ChannelOpenResponse, IbcAckCallbackMsg, IbcAcknowledgement, IbcBasicResponse, IbcCallbackRequest, IbcChannel, IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, - IbcDestinationCallbackMsg, IbcDstCallback, IbcEndpoint, IbcFee, IbcMsg, IbcOrder, IbcPacket, + IbcDestinationCallbackMsg, IbcDstCallback, IbcEndpoint, IbcMsg, IbcOrder, IbcPacket, IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, IbcSourceCallbackMsg, IbcSrcCallback, IbcTimeout, IbcTimeoutBlock, IbcTimeoutCallbackMsg, TransferMsgBuilder, @@ -86,15 +88,16 @@ pub use crate::metadata::{DenomMetadata, DenomUnit}; pub use crate::msgpack::{from_msgpack, to_msgpack_binary, to_msgpack_vec}; pub use crate::never::Never; pub use crate::pagination::PageRequest; +#[allow(deprecated)] +pub use crate::query::FeeEnabledChannelResponse; pub use crate::query::{ AllBalanceResponse, AllDelegationsResponse, AllDenomMetadataResponse, AllValidatorsResponse, BalanceResponse, BankQuery, BondedDenomResponse, ChannelResponse, CodeInfoResponse, ContractInfoResponse, CustomQuery, DecCoin, Delegation, DelegationResponse, DelegationRewardsResponse, DelegationTotalRewardsResponse, DelegatorReward, DelegatorValidatorsResponse, DelegatorWithdrawAddressResponse, DenomMetadataResponse, - DistributionQuery, FeeEnabledChannelResponse, FullDelegation, GrpcQuery, IbcQuery, - ListChannelsResponse, PortIdResponse, QueryRequest, StakingQuery, SupplyResponse, Validator, - ValidatorResponse, WasmQuery, + DistributionQuery, FullDelegation, GrpcQuery, IbcQuery, ListChannelsResponse, PortIdResponse, + QueryRequest, StakingQuery, SupplyResponse, Validator, ValidatorResponse, WasmQuery, }; #[cfg(all(feature = "stargate", feature = "cosmwasm_1_2"))] pub use crate::results::WeightedVoteOption; diff --git a/packages/std/src/metadata.rs b/packages/std/src/metadata.rs index b741f32099..d21936f6c8 100644 --- a/packages/std/src/metadata.rs +++ b/packages/std/src/metadata.rs @@ -1,5 +1,5 @@ use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use crate::prelude::*; @@ -7,6 +7,7 @@ use crate::prelude::*; #[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq, JsonSchema)] pub struct DenomMetadata { pub description: String, + #[serde(deserialize_with = "deserialize_null_default")] pub denom_units: Vec, pub base: String, pub display: String, @@ -21,5 +22,287 @@ pub struct DenomMetadata { pub struct DenomUnit { pub denom: String, pub exponent: u32, + #[serde(deserialize_with = "deserialize_null_default")] pub aliases: Vec, } + +// Deserialize a field that is null, defaulting to the type's default value. +// Panic if the field is missing. +fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result +where + T: Default + Deserialize<'de>, + D: Deserializer<'de>, +{ + let opt = Option::deserialize(deserializer)?; + Ok(opt.unwrap_or_default()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{DenomMetadata, DenomUnit}; + use serde_json::{json, Error}; + + #[test] + fn deserialize_denom_metadata_with_null_fields_works() { + // Test case with null denom_units - should deserialize as empty vec + let json_with_null_denom_units = json!({ + "description": "Test Token", + "denom_units": null, + "base": "utest", + "display": "TEST", + "name": "Test Token", + "symbol": "TEST", + "uri": "https://test.com", + "uri_hash": "hash" + }); + + let metadata_null_denom_units: DenomMetadata = + serde_json::from_value(json_with_null_denom_units).unwrap(); + assert_eq!( + metadata_null_denom_units.denom_units, + Vec::::new() + ); + + // Test normal case with provided denom_units + let json_with_units = json!({ + "description": "Test Token", + "denom_units": [ + { + "denom": "utest", + "exponent": 6, + "aliases": ["microtest"] + } + ], + "base": "utest", + "display": "TEST", + "name": "Test Token", + "symbol": "TEST", + "uri": "https://test.com", + "uri_hash": "hash" + }); + + let metadata_with_units: DenomMetadata = serde_json::from_value(json_with_units).unwrap(); + assert_eq!(metadata_with_units.denom_units.len(), 1); + assert_eq!(metadata_with_units.denom_units[0].denom, "utest"); + + // Test with null aliases inside denom_units - should deserialize as empty vec + let json_with_null_aliases = json!({ + "description": "Test Token", + "denom_units": [ + { + "denom": "utest", + "exponent": 6, + "aliases": null + } + ], + "base": "utest", + "display": "TEST", + "name": "Test Token", + "symbol": "TEST", + "uri": "https://test.com", + "uri_hash": "hash" + }); + + let metadata_with_null_aliases: DenomMetadata = + serde_json::from_value(json_with_null_aliases).unwrap(); + assert_eq!(metadata_with_null_aliases.denom_units.len(), 1); + assert_eq!( + metadata_with_null_aliases.denom_units[0].aliases, + Vec::::new() + ); + } + + #[test] + fn deserialize_denom_metadata_with_missing_fields_fails() { + // Missing denom_units should be treated like null + let json_missing_denom_units = json!({ + "description": "Test Token", + "base": "utest", + "display": "TEST", + "name": "Test Token", + "symbol": "TEST", + "uri": "https://test.com", + "uri_hash": "hash" + }); + + let metadata: Result = + serde_json::from_value(json_missing_denom_units); + assert!(metadata.is_err()); + + let json_missing_alias = json!({ + "description": "Test Token", + "base": "utest", + "denom_units": [ + { + "denom": "utest", + "exponent": 6, + } + ], + "display": "TEST", + "name": "Test Token", + "symbol": "TEST", + "uri": "https://test.com", + "uri_hash": "hash" + }); + + let metadata_missing_alias: Result = + serde_json::from_value(json_missing_alias); + assert!(metadata_missing_alias.is_err()); + } + + #[test] + fn query_denom_metadata_with_null_denom_units_works() { + // Test case with null denom_units - should deserialize as empty vec + let json_with_null_denom_units = json!({ + "description": "Test Token", + "denom_units": null, + "base": "utest", + "display": "TEST", + "name": "Test Token", + "symbol": "TEST", + "uri": "https://test.com", + "uri_hash": "hash" + }); + + let metadata_with_null_denom_units: DenomMetadata = + serde_json::from_value(json_with_null_denom_units).unwrap(); + assert_eq!( + metadata_with_null_denom_units.denom_units, + Vec::::new() + ); + + // Test normal case with provided denom_units + let json_with_units = json!({ + "description": "Test Token", + "denom_units": [ + { + "denom": "utest", + "exponent": 6, + "aliases": ["microtest"] + } + ], + "base": "utest", + "display": "TEST", + "name": "Test Token", + "symbol": "TEST", + "uri": "https://test.com", + "uri_hash": "hash" + }); + + let metadata_with_units: DenomMetadata = serde_json::from_value(json_with_units).unwrap(); + assert_eq!(metadata_with_units.denom_units.len(), 1); + assert_eq!(metadata_with_units.denom_units[0].denom, "utest"); + assert_eq!(metadata_with_units.denom_units[0].aliases.len(), 1); + assert_eq!(metadata_with_units.denom_units[0].aliases[0], "microtest"); + + // Test with null aliases inside denom_units - should deserialize as empty vec + let json_with_null_aliases = json!({ + "description": "Test Token", + "denom_units": [ + { + "denom": "utest", + "exponent": 6, + "aliases": null + } + ], + "base": "utest", + "display": "TEST", + "name": "Test Token", + "symbol": "TEST", + "uri": "https://test.com", + "uri_hash": "hash" + }); + + let metadata_with_null_aliases: DenomMetadata = + serde_json::from_value(json_with_null_aliases).unwrap(); + assert_eq!(metadata_with_null_aliases.denom_units.len(), 1); + assert_eq!( + metadata_with_null_aliases.denom_units[0].aliases, + Vec::::new() + ); + } + + #[test] + fn query_denom_metadata_with_missing_fields_fails() { + // Missing denom_units should throw an error + let json_missing_denom_units = json!({ + "description": "Test Token", + "base": "utest", + "display": "TEST", + "name": "Test Token", + "symbol": "TEST", + "uri": "https://test.com", + "uri_hash": "hash" + }); + + let json_missing_denom_units_metadata: Result = + serde_json::from_value(json_missing_denom_units); + assert!(json_missing_denom_units_metadata.is_err()); + + // Missing aliases field should throw an error + let json_missing_aliases = json!({ + "description": "Test Token", + "denom_units": [ + { + "denom": "utest", + "exponent": 6 + } + ], + "base": "utest", + "display": "TEST", + "name": "Test Token", + "symbol": "TEST", + "uri": "https://test.com", + "uri_hash": "hash" + }); + + let missing_aliases_metadata: Result = + serde_json::from_value(json_missing_aliases); + assert!(missing_aliases_metadata.is_err()); + } + + #[test] + fn query_denom_metadata_with_mixed_null_and_value_works() { + // Test with multiple denom units, some with null aliases and some with values + let mixed_json = json!({ + "description": "Mixed Token", + "denom_units": [ + { + "denom": "unit1", + "exponent": 0, + "aliases": null + }, + { + "denom": "unit2", + "exponent": 6, + "aliases": ["microunit", "u"] + }, + { + "denom": "unit3", + "exponent": 9, + "aliases": [] + } + ], + "base": "unit1", + "display": "MIXED", + "name": "Mixed Token", + "symbol": "MIX", + "uri": "https://mixed.token", + "uri_hash": "hash123" + }); + + let metadata: DenomMetadata = serde_json::from_value(mixed_json).unwrap(); + + // First denom unit has null aliases, should be empty vec + assert!(metadata.denom_units[0].aliases.is_empty()); + + // Second has two aliases + assert_eq!(metadata.denom_units[1].aliases.len(), 2); + assert_eq!(metadata.denom_units[1].aliases[0], "microunit"); + assert_eq!(metadata.denom_units[1].aliases[1], "u"); + + // Third has explicitly empty aliases + assert!(metadata.denom_units[2].aliases.is_empty()); + } +} diff --git a/packages/std/src/query/ibc.rs b/packages/std/src/query/ibc.rs index 123156ea24..80f271c284 100644 --- a/packages/std/src/query/ibc.rs +++ b/packages/std/src/query/ibc.rs @@ -36,6 +36,10 @@ pub enum IbcQuery { /// /// Returns a `FeeEnabledChannelResponse`. #[cfg(feature = "cosmwasm_2_2")] + #[deprecated( + since = "2.2.3", + note = "IBC fees have been removed from ibc-go `v10`, which is used in wasmd `v0.55.0`." + )] FeeEnabledChannel { port_id: Option, channel_id: String, @@ -68,6 +72,10 @@ impl_response_constructor!(ChannelResponse, channel: Option); #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[non_exhaustive] +#[deprecated( + since = "2.2.3", + note = "IBC fees have been removed from ibc-go `v10`, which is used in wasmd `v0.55.0`." +)] pub struct FeeEnabledChannelResponse { pub fee_enabled: bool, } diff --git a/packages/std/src/testing/mock.rs b/packages/std/src/testing/mock.rs index 0d1c35565f..bfe606ef4a 100644 --- a/packages/std/src/testing/mock.rs +++ b/packages/std/src/testing/mock.rs @@ -958,6 +958,7 @@ impl IbcQuerier { to_json_binary(&res).into() } #[cfg(feature = "cosmwasm_2_2")] + #[allow(deprecated)] IbcQuery::FeeEnabledChannel { .. } => { use crate::query::FeeEnabledChannelResponse; // for now, we always return true