Repository: CosmWasm/cosmwasm (rust)

Issue:
`RawRange` query
We currently have a way to read from and range through our own storage, but for external storage we can only read a single key using `WasmQuery::Raw`.

We should add a way to query a range of keys from an external contract.
This should be paginated to be able to avoid gas / memory exhaustion if the external contract is not fully trusted.

Something like this?
```rust
RawRange {
    /// Inclusive bound
    start: Option<Vec<u8>>,
    /// Maximum number of elements to return
    limit: u16,
    order: Order,
}
```

I'm a little conflicted here because on the one side it would be nice to stay close to the way `Storage::range` works (start, end, order), but with that it's not as easy to control the amount of items you get. Using the `end` key for that is impractical because it leads to very short responses in sparsely populated maps. We could add an additional `limit` to that, but then it becomes a bit complicated to reason about.

Pull request:
Implement `RawRange` query
closes #2190 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c1ed9f87b..76bb24adc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -33,6 +33,8 @@ and this project adheres to
 - cosmwasm-vm: Add `ibc2_packet_send` entrypoint ([#2477])
 - cosmwasm-std: Add Tx hash to TransactionInfo and make it non exhaustive
   ([#2480])
+- cosmwasm-std: Add `WasmQuery::RawRange` query to allow querying raw storage
+  ranges of different contracts. ([#2471])
 
 ## Changed
 
@@ -130,6 +132,7 @@ and this project adheres to
 [#2454]: https://github.com/CosmWasm/cosmwasm/pull/2454
 [#2458]: https://github.com/CosmWasm/cosmwasm/pull/2458
 [#2467]: https://github.com/CosmWasm/cosmwasm/pull/2467
+[#2471]: https://github.com/CosmWasm/cosmwasm/pull/2471
 [#2472]: https://github.com/CosmWasm/cosmwasm/pull/2472
 [#2473]: https://github.com/CosmWasm/cosmwasm/pull/2473
 [#2477]: https://github.com/CosmWasm/cosmwasm/pull/2477
diff --git a/docs/CAPABILITIES-BUILT-IN.md b/docs/CAPABILITIES-BUILT-IN.md
index 992fc99af..ae3f38c4a 100644
--- a/docs/CAPABILITIES-BUILT-IN.md
+++ b/docs/CAPABILITIES-BUILT-IN.md
@@ -33,3 +33,5 @@ might define others.
   `migrate` entrypoint, as well as IBC Fees support with `IbcMsg::PayPacketFee`,
   `IbcMsg::PayPacketFeeAsync` and `IbcQuery::FeeEnabledChannel`. Only chains
   running CosmWasm `2.2.0` or higher support this.
+- `cosmwasm_3_0` enables `WasmQuery::RawRange`. Only chains running CosmWasm
+  `3.0.0` or higher support this.
diff --git a/packages/go-gen/Cargo.toml b/packages/go-gen/Cargo.toml
index 993b0b096..fb11eabeb 100644
--- a/packages/go-gen/Cargo.toml
+++ b/packages/go-gen/Cargo.toml
@@ -10,7 +10,7 @@ release = false
 
 [dependencies]
 cosmwasm-std = { version = "3.0.0-ibc2.0", path = "../std", features = [
-    "cosmwasm_2_2",
+    "cosmwasm_3_0",
     "staking",
     "stargate",
 ] }
diff --git a/packages/go-gen/src/go.rs b/packages/go-gen/src/go.rs
index 5f762ec68..726a3948a 100644
--- a/packages/go-gen/src/go.rs
+++ b/packages/go-gen/src/go.rs
@@ -50,8 +50,16 @@ impl Display for GoField {
             self.ty,
             self.rust_name
         )?;
-        if let Nullability::OmitEmpty | Nullability::Nullable = self.ty.nullability {
-            f.write_str(",omitempty")?;
+        match self.ty.nullability {
+            Nullability::OmitEmpty => {
+                f.write_str(",omitempty")?;
+            }
+            Nullability::Nullable if !self.ty.is_slice() => {
+                // if the type is nullable, we need to use a pointer type
+                // and add `omitempty` to the json tag
+                f.write_str(",omitempty")?;
+            }
+            _ => {}
         }
         f.write_str("\"`")
     }
@@ -100,12 +108,17 @@ impl GoType {
         ];
         BASIC_GO_TYPES.contains(&&*self.name)
     }
+
+    pub fn is_slice(&self) -> bool {
+        self.name.starts_with("[]")
+    }
 }
 
 impl Display for GoType {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        if self.nullability == Nullability::Nullable && !self.is_basic_type() {
+        if self.nullability == Nullability::Nullable && !self.is_basic_type() && !self.is_slice() {
             // if the type is nullable and not a basic type, use a pointer
+            // slices are already pointers, so we don't need to do anything for them
             f.write_char('*')?;
         }
         f.write_str(&self.name)
diff --git a/packages/go-gen/src/main.rs b/packages/go-gen/src/main.rs
index 117488442..c5243eb20 100644
--- a/packages/go-gen/src/main.rs
+++ b/packages/go-gen/src/main.rs
@@ -11,7 +11,7 @@ mod schema;
 mod utils;
 
 fn main() -> Result<()> {
-    let root = cosmwasm_schema::schema_for!(cosmwasm_std::Reply);
+    let root = cosmwasm_schema::schema_for!(cosmwasm_std::RawRangeResponse);
 
     let code = generate_go(root)?;
     println!("{}", code);
@@ -198,6 +198,7 @@ mod tests {
 
     use super::*;
 
+    #[track_caller]
     fn assert_code_eq(actual: String, expected: &str) {
         let actual_no_ws = actual.split_whitespace().collect::<Vec<_>>();
         let expected_no_ws = expected.split_whitespace().collect::<Vec<_>>();
@@ -210,6 +211,7 @@ mod tests {
         );
     }
 
+    #[track_caller]
     fn assert_code_eq_ignore_docs(actual: String, expected: &str) {
         let actual_filtered = actual
             .lines()
@@ -251,7 +253,7 @@ mod tests {
                 Binary []byte `json:"binary"`
                 Checksum Checksum `json:"checksum"`
                 HexBinary string `json:"hex_binary"`
-                NestedBinary Array[*[]byte] `json:"nested_binary"`
+                NestedBinary Array[[]byte] `json:"nested_binary"`
                 Uint128 string `json:"uint128"`
             }"#,
         );
@@ -340,7 +342,7 @@ mod tests {
         compare_codes!(cosmwasm_std::SupplyResponse);
         compare_codes!(cosmwasm_std::BalanceResponse);
         compare_codes!(cosmwasm_std::DenomMetadataResponse);
-        // compare_codes!(cosmwasm_std::AllDenomMetadataResponse); // uses `[]byte` instead of `*[]byte`
+        // compare_codes!(cosmwasm_std::AllDenomMetadataResponse); // uses slice instead of `Array` type
         // staking
         compare_codes!(cosmwasm_std::BondedDenomResponse);
         compare_codes!(cosmwasm_std::AllDelegationsResponse);
@@ -355,6 +357,7 @@ mod tests {
         // wasm
         compare_codes!(cosmwasm_std::ContractInfoResponse);
         compare_codes!(cosmwasm_std::CodeInfoResponse);
+        compare_codes!(cosmwasm_std::RawRangeResponse);
     }
 
     #[test]
@@ -475,6 +478,8 @@ mod tests {
 
         #[cw_serde]
         struct D {
+            // this should not get an `omitempty` because that prevents us from distinguishing between
+            // `None` and `Some(vec![])`
             d: Option<Vec<String>>,
             nested: Vec<Option<Vec<String>>>,
         }
@@ -483,8 +488,8 @@ mod tests {
             code,
             r#"
             type D struct {
-                D *[]string `json:"d,omitempty"`
-                Nested Array[*[]string] `json:"nested"`
+                D []string `json:"d"`
+                Nested Array[[]string] `json:"nested"`
             }"#,
         );
     }
diff --git a/packages/go-gen/src/schema.rs b/packages/go-gen/src/schema.rs
index 198675b99..7caf6f6e6 100644
--- a/packages/go-gen/src/schema.rs
+++ b/packages/go-gen/src/schema.rs
@@ -212,8 +212,8 @@ pub fn type_from_instance_type(
 
         // for nullable array item types, we have to use a pointer type, even for basic types,
         // so we can pass null as elements
-        // otherwise they would just be omitted from the array
-        let maybe_ptr = if item_type.nullability == Nullability::Nullable {
+        // otherwise they would just be omitted from the array, unless it's a slice itself
+        let maybe_ptr = if item_type.nullability == Nullability::Nullable && !item_type.is_slice() {
             "*"
         } else {
             ""
@@ -246,6 +246,16 @@ pub fn array_item_type(
         Some(SingleOrVec::Single(array_validation)) => {
             schema_object_type(array_validation.object()?, type_context, additional_structs)
         }
+        Some(SingleOrVec::Vec(v))
+            if v.len() == 1 || v.len() > 1 && v.windows(2).all(|w| w[0] == w[1]) =>
+        {
+            // all items are the same type
+            schema_object_type(
+                v.first().unwrap().object()?,
+                type_context,
+                additional_structs,
+            )
+        }
         _ => bail!("array type with non-singular item type is not supported"),
     }
 }
@@ -288,6 +298,7 @@ pub fn custom_type_of(ty: &str) -> Option<&str> {
         "Uint128" => Some("string"),
         "Uint256" => Some("string"),
         "Uint512" => Some("string"),
+        "Order" => Some("string"),
         "Int64" => Some("Int64"),
         "Int128" => Some("string"),
         "Int256" => Some("string"),
diff --git a/packages/std/Cargo.toml b/packages/std/Cargo.toml
index e0cbf954a..2240eca1c 100644
--- a/packages/std/Cargo.toml
+++ b/packages/std/Cargo.toml
@@ -9,7 +9,7 @@ license = "Apache-2.0"
 readme = "README.md"
 
 [package.metadata.docs.rs]
-features = ["cosmwasm_2_2", "staking", "stargate", "ibc2"]
+features = ["cosmwasm_3_0", "staking", "stargate", "ibc2"]
 
 [features]
 default = ["exports", "iterator", "std"]
@@ -55,6 +55,9 @@ cosmwasm_2_1 = ["cosmwasm_2_0"]
 # This enables functionality that is only available on 2.2 chains.
 # It adds `IbcMsg::PayPacketFee` and `IbcMsg::PayPacketFeeAsync`.
 cosmwasm_2_2 = ["cosmwasm_2_1"]
+# This enables functionality that is only available on 3.0 chains.
+# It adds `WasmQuery::RawRange`.
+cosmwasm_3_0 = ["cosmwasm_2_2"]
 
 [dependencies]
 base64 = "0.22.0"
diff --git a/packages/std/src/exports/exports.rs b/packages/std/src/exports/exports.rs
index cb8f2c031..9be7ab2aa 100644
--- a/packages/std/src/exports/exports.rs
+++ b/packages/std/src/exports/exports.rs
@@ -82,6 +82,10 @@ extern "C" fn requires_cosmwasm_2_1() {}
 #[no_mangle]
 extern "C" fn requires_cosmwasm_2_2() {}
 
+#[cfg(feature = "cosmwasm_3_0")]
+#[no_mangle]
+extern "C" fn requires_cosmwasm_3_0() {}
+
 /// interface_version_* exports mark which Wasm VM interface level this contract is compiled for.
 /// They can be checked by cosmwasm_vm.
 /// Update this whenever the Wasm VM interface breaks.
diff --git a/packages/std/src/lib.rs b/packages/std/src/lib.rs
index 739cac9af..db7799147 100644
--- a/packages/std/src/lib.rs
+++ b/packages/std/src/lib.rs
@@ -98,9 +98,10 @@ pub use crate::query::{
     CustomQuery, DecCoin, Delegation, DelegationResponse, DelegationRewardsResponse,
     DelegationTotalRewardsResponse, DelegatorReward, DelegatorValidatorsResponse,
     DelegatorWithdrawAddressResponse, DenomMetadataResponse, DistributionQuery, FullDelegation,
-    GrpcQuery, IbcQuery, PortIdResponse, QueryRequest, StakingQuery, SupplyResponse, Validator,
-    ValidatorResponse, WasmQuery,
+    GrpcQuery, IbcQuery, PortIdResponse, QueryRequest, RawRangeEntry, RawRangeResponse,
+    StakingQuery, SupplyResponse, Validator, ValidatorResponse, WasmQuery,
 };
+
 #[cfg(all(feature = "stargate", feature = "cosmwasm_1_2"))]
 pub use crate::results::WeightedVoteOption;
 pub use crate::results::{
diff --git a/packages/std/src/query/wasm.rs b/packages/std/src/query/wasm.rs
index f9cab5585..dc1e5aed3 100644
--- a/packages/std/src/query/wasm.rs
+++ b/packages/std/src/query/wasm.rs
@@ -2,7 +2,11 @@ use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 
 use crate::prelude::*;
+#[cfg(all(feature = "cosmwasm_3_0", feature = "iterator"))]
+use crate::storage_keys::{range_to_bounds, ToByteVec};
 use crate::{Addr, Binary, Checksum};
+#[cfg(all(feature = "cosmwasm_3_0", feature = "iterator"))]
+use core::ops::RangeBounds;
 
 use super::query_response::QueryResponseType;
 
@@ -32,6 +36,60 @@ pub enum WasmQuery {
     /// Returns a [`CodeInfoResponse`] with metadata of the code
     #[cfg(feature = "cosmwasm_1_2")]
     CodeInfo { code_id: u64 },
+    /// Queries a range of keys from the storage of a (different) contract,
+    /// returning a [`RawRangeResponse`].
+    ///
+    /// This is a low-level query that allows you to query the storage of another contract.
+    /// Please keep in mind that the contract you are querying might change its storage layout using
+    /// migrations, which could break your queries, so it is recommended to only use this for
+    /// contracts you control.
+    #[cfg(all(feature = "cosmwasm_3_0", feature = "iterator"))]
+    RawRange {
+        /// The address of the contract to query
+        contract_addr: String,
+        /// Inclusive start bound. This is the first key you would like to get data for.
+        ///
+        /// If `start` is lexicographically greater than or equal to `end`,
+        /// an empty range is described, mo matter of the order.
+        start: Option<Binary>,
+        /// Exclusive end bound. This is the key after the last key you would like to get data for.
+        end: Option<Binary>,
+        /// Maximum number of elements to return.
+        ///
+        /// Make sure to set a reasonable limit to avoid running out of memory or into
+        /// the deserialization limits of the VM. Also keep in mind that these limitations depend
+        /// on the full JSON size of the response type.
+        limit: u16,
+        /// The order in which you want to receive the key-value pairs.
+        order: crate::Order,
+    },
+}
+
+impl WasmQuery {
+    /// Creates a new [`WasmQuery::RawRange`] from the given parameters.
+    ///
+    /// This takes a [`RangeBounds`] to allow for specifying the range in a more idiomatic way.
+    #[cfg(all(feature = "cosmwasm_3_0", feature = "iterator"))]
+    pub fn raw_range<'a, R, B>(
+        contract_addr: impl Into<String>,
+        range: R,
+        limit: u16,
+        order: crate::Order,
+    ) -> Self
+    where
+        R: RangeBounds<&'a B>,
+        B: ToByteVec + ?Sized + 'a,
+    {
+        let (start, end) = range_to_bounds(&range);
+
+        WasmQuery::RawRange {
+            contract_addr: contract_addr.into(),
+            start: start.map(Binary::new),
+            end: end.map(Binary::new),
+            limit,
+            order,
+        }
+    }
 }
 
 #[non_exhaustive]
@@ -88,6 +146,23 @@ impl_hidden_constructor!(
 
 impl QueryResponseType for CodeInfoResponse {}
 
+#[non_exhaustive]
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
+pub struct RawRangeResponse {
+    /// The key-value pairs
+    pub data: Vec<RawRangeEntry>,
+    /// `None` if there are no more key-value pairs within the given key range.
+    pub next_key: Option<Binary>,
+}
+
+impl_hidden_constructor!(
+    RawRangeResponse,
+    data: Vec<RawRangeEntry>,
+    next_key: Option<Binary>
+);
+
+pub type RawRangeEntry = (Binary, Binary);
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -152,4 +227,56 @@ mod tests {
             r#"{"code_id":67,"creator":"jane","checksum":"f7bb7b18fb01bbf425cf4ed2cd4b7fb26a019a7fc75a4dc87e8a0b768c501f00"}"#,
         );
     }
+
+    #[test]
+    #[cfg(all(feature = "cosmwasm_3_0", feature = "iterator"))]
+    fn raw_range_constructor_works() {
+        use crate::Order;
+
+        let query = WasmQuery::raw_range(
+            "contract_addr",
+            &b"asdf"[..]..&b"asdz"[..],
+            100,
+            Order::Ascending,
+        );
+
+        assert_eq!(
+            query,
+            WasmQuery::RawRange {
+                contract_addr: "contract_addr".to_string(),
+                start: Some(Binary::from(b"asdf")),
+                end: Some(Binary::from(b"asdz")),
+                limit: 100,
+                order: Order::Ascending,
+            }
+        );
+
+        let query = WasmQuery::raw_range("contract_addr", b"asdf"..=b"asdz", 100, Order::Ascending);
+        assert_eq!(
+            query,
+            WasmQuery::RawRange {
+                contract_addr: "contract_addr".to_string(),
+                start: Some(Binary::from(b"asdf")),
+                end: Some(Binary::from(b"asdz\0")),
+                limit: 100,
+                order: Order::Ascending,
+            }
+        );
+    }
+
+    #[test]
+    fn raw_range_response_serialization() {
+        let response = RawRangeResponse {
+            data: vec![
+                (Binary::from(b"key"), Binary::from(b"value")),
+                (Binary::from(b"foo"), Binary::from(b"bar")),
+            ],
+            next_key: Some(Binary::from(b"next")),
+        };
+        let json = to_json_binary(&response).unwrap();
+        assert_eq!(
+            String::from_utf8_lossy(&json),
+            r#"{"data":[["a2V5","dmFsdWU="],["Zm9v","YmFy"]],"next_key":"bmV4dA=="}"#,
+        );
+    }
 }
diff --git a/packages/std/src/results/cosmos_msg.rs b/packages/std/src/results/cosmos_msg.rs
index 85bbe23d1..600a5aa3f 100644
--- a/packages/std/src/results/cosmos_msg.rs
+++ b/packages/std/src/results/cosmos_msg.rs
@@ -626,7 +626,7 @@ mod tests {
     }
 
     #[test]
-    #[cfg(feature = "cosmwasm_1_3")]
+    #[cfg(all(feature = "cosmwasm_1_3", feature = "staking"))]
     fn msg_distribution_serializes_to_correct_json() {
         // FundCommunityPool
         let fund_coins = vec![coin(200, "feathers"), coin(200, "stones")];
diff --git a/packages/std/src/storage_keys/mod.rs b/packages/std/src/storage_keys/mod.rs
index 630feaa18..415c2a605 100644
--- a/packages/std/src/storage_keys/mod.rs
+++ b/packages/std/src/storage_keys/mod.rs
@@ -1,5 +1,9 @@
 mod length_prefixed;
+#[cfg(all(feature = "cosmwasm_3_0", feature = "iterator"))]
+mod range;
 
 // Please note that the entire storage_keys module is public. So be careful
 // when adding elements here.
 pub use length_prefixed::{namespace_with_key, to_length_prefixed, to_length_prefixed_nested};
+#[cfg(all(feature = "cosmwasm_3_0", feature = "iterator"))]
+pub(crate) use range::{range_to_bounds, ToByteVec};
diff --git a/packages/std/src/storage_keys/range.rs b/packages/std/src/storage_keys/range.rs
new file mode 100644
index 000000000..91b85e454
--- /dev/null
+++ b/packages/std/src/storage_keys/range.rs
@@ -0,0 +1,141 @@
+use core::ops::{Bound, RangeBounds};
+
+use crate::Binary;
+
+pub trait ToByteVec {
+    fn to_byte_vec(&self) -> Vec<u8>;
+}
+impl ToByteVec for Vec<u8> {
+    fn to_byte_vec(&self) -> Vec<u8> {
+        self.clone()
+    }
+}
+impl ToByteVec for [u8] {
+    fn to_byte_vec(&self) -> Vec<u8> {
+        self.to_vec()
+    }
+}
+impl<const N: usize> ToByteVec for [u8; N] {
+    fn to_byte_vec(&self) -> Vec<u8> {
+        self.to_vec()
+    }
+}
+impl ToByteVec for Binary {
+    fn to_byte_vec(&self) -> Vec<u8> {
+        self.to_vec()
+    }
+}
+
+/// Converts any range to start and end bounds for ranging through storage.
+/// The start bound is inclusive, the end bound is exclusive.
+pub fn range_to_bounds<'a, R, B>(range: &R) -> (Option<Vec<u8>>, Option<Vec<u8>>)
+where
+    R: RangeBounds<&'a B>,
+    B: ToByteVec + 'a + ?Sized,
+{
+    let start = match range.start_bound() {
+        Bound::Included(start) => Some(start.to_byte_vec()),
+        Bound::Excluded(start) => Some(key_after(start.to_byte_vec())),
+        Bound::Unbounded => None,
+    };
+    let end = match range.end_bound() {
+        Bound::Included(end) => Some(key_after(end.to_byte_vec())),
+        Bound::Excluded(end) => Some(end.to_byte_vec()),
+        Bound::Unbounded => None,
+    };
+    (start, end)
+}
+
+/// Returns the key after the given key.
+///
+/// Reuses the given vector.
+fn key_after(mut key: Vec<u8>) -> Vec<u8> {
+    key.push(0);
+    key
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::{testing::MockStorage, Order, Storage};
+
+    use super::*;
+
+    #[test]
+    fn range_to_bounds_works() {
+        let mut storage = MockStorage::new();
+
+        let keys: &[&[u8]] = &[
+            &[1, 2, 3],
+            &[1, 2, 4],
+            &[1, 2, 5],
+            &[1, 2, 6],
+            &[1, 2, 7],
+            &[1, 2, 7, 0],
+            &[1, 2, 7, 1],
+            &[1, 2, 7, 2],
+            &[1, 2, 8],
+            &[1, 2, 8, 0],
+            &[1, 2, 8, 1],
+        ];
+        // map every key to its index
+        for (i, &key) in keys.iter().enumerate() {
+            storage.set(key, &[i as u8]);
+        }
+
+        // check the range between any two keys inside the storage
+        for (idx0, &key0) in keys.iter().enumerate() {
+            for (idx1, &key1) in keys.iter().enumerate() {
+                // key0..key1 should have idx0..idx1 as values
+                assert_range(&storage, key0..key1, (idx0..idx1).map(|idx| idx as u8));
+
+                // key0..=key1 should have idx0..=idx1 as values
+                assert_range(&storage, key0..=key1, (idx0..=idx1).map(|idx| idx as u8));
+            }
+
+            // key0.. should have idx0.. as values
+            assert_range(&storage, key0.., (idx0..keys.len()).map(|idx| idx as u8));
+            // ..key0 should have 0..idx0 as values
+            assert_range(&storage, ..key0, (0..idx0).map(|idx| idx as u8));
+            // ..=key0 should have 0..=idx0 as values
+            assert_range(&storage, ..=key0, (0..=idx0).map(|idx| idx as u8));
+        }
+
+        // 0..not_in_storage should have range from start to last key before not_in_storage
+        let zero: &[u8] = &[0u8];
+        let not_in_storage = &[1u8, 2, 7, 3];
+        assert_range(&storage, zero..not_in_storage, 0u8..=7);
+        assert_range(&storage, zero..=not_in_storage, 0u8..=7);
+
+        // 0..after_last_key should have full range
+        let after_last_key: &[u8] = &[1u8, 2, 8, 2];
+        assert_range(&storage, zero..after_last_key, 0u8..keys.len() as u8);
+        assert_range(&storage, zero..=after_last_key, 0u8..keys.len() as u8);
+
+        // full range
+        assert_range(&storage, .., 0u8..keys.len() as u8);
+
+        fn assert_range<'a>(
+            storage: &MockStorage,
+            range: impl RangeBounds<&'a [u8]>,
+            expected_values: impl DoubleEndedIterator<Item = u8> + Clone,
+        ) {
+            let (s, e) = range_to_bounds(&range);
+            // ascending
+            let values = storage
+                .range_values(s.as_deref(), e.as_deref(), Order::Ascending)
+                .collect::<Vec<_>>();
+            assert_eq!(
+                values,
+                expected_values.clone().map(|v| vec![v]).collect::<Vec<_>>()
+            );
+            // descending
+            let values = storage
+                .range_values(s.as_deref(), e.as_deref(), Order::Descending)
+                .collect::<Vec<_>>();
+            assert_eq!(
+                values,
+                expected_values.rev().map(|v| vec![v]).collect::<Vec<_>>()
+            );
+        }
+    }
+}


Test patch:
diff --git a/packages/go-gen/tests/cosmwasm_std__RawRangeResponse.go b/packages/go-gen/tests/cosmwasm_std__RawRangeResponse.go
new file mode 100644
index 000000000..11dbc5718
--- /dev/null
+++ b/packages/go-gen/tests/cosmwasm_std__RawRangeResponse.go
@@ -0,0 +1,6 @@
+type RawRangeResponse struct {
+	// The key-value pairs
+	Data Array[Array[[]byte]] `json:"data"`
+	// `None` if there are no more key-value pairs within the given key range.
+	NextKey []byte `json:"next_key"`
+}
\ No newline at end of file
diff --git a/packages/go-gen/tests/cosmwasm_std__WasmQuery.go b/packages/go-gen/tests/cosmwasm_std__WasmQuery.go
index 84f3f453c..c6a3c88c8 100644
--- a/packages/go-gen/tests/cosmwasm_std__WasmQuery.go
+++ b/packages/go-gen/tests/cosmwasm_std__WasmQuery.go
@@ -22,9 +22,27 @@ type CodeInfoQuery struct {
 	CodeID uint64 `json:"code_id"`
 }
 
+type RawRangeQuery struct {
+	// The address of the contract to query
+	ContractAddr string `json:"contract_addr"`
+	// Exclusive end bound. This is the key after the last key you would like to get data for.
+	End []byte `json:"end"`
+	// Maximum number of elements to return.
+	//
+	// Make sure to set a reasonable limit to avoid running out of memory or into the deserialization limits of the VM. Also keep in mind that these limitations depend on the full JSON size of the response type.
+	Limit uint16 `json:"limit"`
+	// The order in which you want to receive the key-value pairs.
+	Order string `json:"order"`
+	// Inclusive start bound. This is the first key you would like to get data for.
+	//
+	// If `start` is lexicographically greater than or equal to `end`, an empty range is described, mo matter of the order.
+	Start []byte `json:"start"`
+}
+
 type WasmQuery struct {
 	Smart        *SmartQuery        `json:"smart,omitempty"`
 	Raw          *RawQuery          `json:"raw,omitempty"`
 	ContractInfo *ContractInfoQuery `json:"contract_info,omitempty"`
 	CodeInfo     *CodeInfoQuery     `json:"code_info,omitempty"`
+	RawRange     *RawRangeQuery     `json:"raw_range,omitempty"`
 }
\ No newline at end of file
diff --git a/packages/std/src/testing/mock.rs b/packages/std/src/testing/mock.rs
index 94e7dab2d..cc9081973 100644
--- a/packages/std/src/testing/mock.rs
+++ b/packages/std/src/testing/mock.rs
@@ -946,6 +946,10 @@ impl Default for WasmQuerier {
                 WasmQuery::CodeInfo { code_id, .. } => {
                     SystemError::NoSuchCode { code_id: *code_id }
                 }
+                #[cfg(all(feature = "cosmwasm_3_0", feature = "iterator"))]
+                WasmQuery::RawRange { contract_addr, .. } => SystemError::NoSuchContract {
+                    addr: contract_addr.clone(),
+                },
             };
             SystemResult::Err(err)
         });
@@ -1418,7 +1422,7 @@ mod tests {
     use crate::coins;
     #[cfg(feature = "cosmwasm_1_3")]
     use crate::DenomUnit;
-    use crate::{coin, instantiate2_address, ContractInfoResponse, HexBinary, Response};
+    use crate::{coin, instantiate2_address, ContractInfoResponse, HexBinary, Response, Storage};
     #[cfg(feature = "staking")]
     use crate::{Decimal, Delegation};
     use base64::{engine::general_purpose, Engine};
@@ -2728,11 +2732,29 @@ mod tests {
             }
         }
 
+        #[cfg(all(feature = "cosmwasm_3_0", feature = "iterator"))]
+        {
+            // By default, querier errors for WasmQuery::RawRange
+            let system_err = querier
+                .query(&WasmQuery::RawRange {
+                    contract_addr: any_addr.clone(),
+                    start: None,
+                    end: None,
+                    limit: 10,
+                    order: crate::Order::Ascending,
+                })
+                .unwrap_err();
+            match system_err {
+                SystemError::NoSuchContract { addr } => assert_eq!(addr, any_addr),
+                err => panic!("Unexpected error: {err:?}"),
+            }
+        }
+
         querier.update_handler(|request| {
             let api = MockApi::default();
             let contract1 = api.addr_make("contract1");
-            let mut storage1 = BTreeMap::<Binary, Binary>::default();
-            storage1.insert(b"the key".into(), b"the value".into());
+            let mut storage1 = MockStorage::new();
+            storage1.set(b"the key", b"the value");
 
             match request {
                 WasmQuery::Raw { contract_addr, key } => {
@@ -2743,7 +2765,7 @@ mod tests {
                     };
                     if addr == contract1 {
                         if let Some(value) = storage1.get(key) {
-                            SystemResult::Ok(ContractResult::Ok(value.clone()))
+                            SystemResult::Ok(ContractResult::Ok(Binary::new(value)))
                         } else {
                             SystemResult::Ok(ContractResult::Ok(Binary::default()))
                         }
@@ -2816,6 +2838,47 @@ mod tests {
                         SystemResult::Err(SystemError::NoSuchCode { code_id })
                     }
                 }
+                #[cfg(all(feature = "cosmwasm_3_0", feature = "iterator"))]
+                WasmQuery::RawRange {
+                    contract_addr,
+                    start,
+                    end,
+                    limit,
+                    order,
+                } => {
+                    let Ok(addr) = api.addr_validate(contract_addr) else {
+                        return SystemResult::Err(SystemError::NoSuchContract {
+                            addr: contract_addr.clone(),
+                        });
+                    };
+                    if addr == contract1 {
+                        let mut data: Vec<_> = storage1
+                            .range(
+                                start.as_ref().map(Binary::as_slice),
+                                end.as_ref().map(Binary::as_slice),
+                                *order,
+                            )
+                            .take(*limit as usize + 1) // take one more entry than limit
+                            .map(|(key, value)| (Binary::new(key), Binary::new(value)))
+                            .collect();
+
+                        // if we have more than limit, there are more entries to fetch
+                        let next_key = if data.len() > *limit as usize {
+                            data.pop().map(|(key, _)| key)
+                        } else {
+                            None
+                        };
+                        let raw_range_response = crate::RawRangeResponse { data, next_key };
+
+                        SystemResult::Ok(ContractResult::Ok(
+                            to_json_binary(&raw_range_response).unwrap(),
+                        ))
+                    } else {
+                        SystemResult::Err(SystemError::NoSuchContract {
+                            addr: contract_addr.clone(),
+                        })
+                    }
+                }
             }
         });
 
@@ -2865,7 +2928,7 @@ mod tests {
 
         // WasmQuery::ContractInfo
         let result = querier.query(&WasmQuery::ContractInfo {
-            contract_addr: contract_addr.into(),
+            contract_addr: contract_addr.clone().into(),
         });
         match result {
             SystemResult::Ok(ContractResult::Ok(value)) => assert_eq!(
@@ -2888,6 +2951,24 @@ mod tests {
                 res => panic!("Unexpected result: {res:?}"),
             }
         }
+
+        #[cfg(all(feature = "cosmwasm_3_0", feature = "iterator"))]
+        {
+            let result = querier.query(&WasmQuery::RawRange {
+                contract_addr: contract_addr.clone().into(),
+                start: Some(Binary::from(b"the key")),
+                end: Some(Binary::from(b"the keyasdf")),
+                limit: 10,
+                order: crate::Order::Ascending,
+            });
+            match result {
+                SystemResult::Ok(ContractResult::Ok(value)) => assert_eq!(
+                    value.as_slice(),
+                    br#"{"data":[["dGhlIGtleQ==","dGhlIHZhbHVl"]],"next_key":null}"#
+                ),
+                res => panic!("Unexpected result: {res:?}"),
+            }
+        }
     }
 
     #[test]
diff --git a/packages/vm/src/testing/instance.rs b/packages/vm/src/testing/instance.rs
index 232dcfadc..f23fe4915 100644
--- a/packages/vm/src/testing/instance.rs
+++ b/packages/vm/src/testing/instance.rs
@@ -98,7 +98,7 @@ impl MockInstanceOptions<'_> {
     fn default_capabilities() -> HashSet<String> {
         #[allow(unused_mut)]
         let mut out = capabilities_from_csv(
-            "ibc2,iterator,staking,cosmwasm_1_1,cosmwasm_1_2,cosmwasm_1_3,cosmwasm_1_4,cosmwasm_2_0,cosmwasm_2_1,cosmwasm_2_2",
+            "ibc2,iterator,staking,cosmwasm_1_1,cosmwasm_1_2,cosmwasm_1_3,cosmwasm_1_4,cosmwasm_2_0,cosmwasm_2_1,cosmwasm_2_2,cosmwasm_3_0",
         );
         #[cfg(feature = "stargate")]
         out.insert("stargate".to_string());