abstract_std/objects/
module_version.rs

1/*!
2Most of the CW* specs are focused on the *public interfaces*
3of the module. The Adapters used for `ExecuteMsg` or `QueryMsg`.
4However, when we wish to migrate or inspect smart module info,
5we need some form of smart module information embedded on state.
6
7This is where ModuleData comes in. It specifies a special Item to
8be stored on disk by all contracts on `instantiate`.
9
10`ModuleInfo` must be stored under the `"module_info"` key which translates
11to `"636F6E74726163745F696E666F"` in hex format.
12Since the state is well-defined, we do not need to support any "smart queries".
13We do provide a helper to construct a "raw query" to read the ContractInfo
14of any CW2-compliant module.
15
16Additionally, it's worth noting that `ModuleData` is utilized by native
17abstract contracts.
18
19For more information on this specification, please check out the
20[README](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw2/README.md).
21 */
22
23use cosmwasm_std::{
24    ensure, ensure_eq, Empty, Querier, QuerierWrapper, QueryRequest, StdResult, Storage, WasmQuery,
25};
26use cw2::{get_contract_version, ContractVersion};
27use cw_storage_plus::Item;
28use semver::Version;
29use serde::{Deserialize, Serialize};
30
31use super::{
32    dependency::{Dependency, DependencyResponse, StaticDependency},
33    storage_namespaces::MODULE_STORAGE_KEY,
34};
35use crate::AbstractError;
36
37// ANCHOR: metadata
38pub const MODULE: Item<ModuleData> = Item::new(MODULE_STORAGE_KEY);
39
40/// Represents metadata for abstract modules and abstract native contracts.
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
42pub struct ModuleData {
43    /// The name of the module, which should be composed of
44    /// the publisher's namespace and module id. eg. `cw-plus:cw20-base`
45    pub module: String,
46    /// Semantic version of the module's crate on release.
47    /// Is used for migration assertions
48    pub version: String,
49    /// List of modules that this module depends on
50    /// along with its version requirements.
51    pub dependencies: Vec<Dependency>,
52    /// URL to data that follows the Abstract metadata standard for
53    /// resolving off-chain module information.
54    pub metadata: Option<String>,
55}
56// ANCHOR_END: metadata
57
58#[cosmwasm_schema::cw_serde]
59pub struct ModuleDataResponse {
60    pub module_id: String,
61    pub version: String,
62    pub dependencies: Vec<DependencyResponse>,
63    pub metadata: Option<String>,
64}
65
66/// set_module_version should be used in instantiate to store the original version, and after a successful
67/// migrate to update it
68pub fn set_module_data<T: Into<String>, U: Into<String>, M: Into<String>>(
69    store: &mut dyn Storage,
70    name: T,
71    version: U,
72    dependencies: &[StaticDependency],
73    metadata: Option<M>,
74) -> StdResult<()> {
75    let val = ModuleData {
76        module: name.into(),
77        version: version.into(),
78        dependencies: dependencies.iter().map(Into::into).collect(),
79        metadata: metadata.map(Into::into),
80    };
81    MODULE.save(store, &val).map_err(Into::into)
82}
83
84/// Assert that the new version is greater than the stored version.
85pub fn assert_contract_upgrade(
86    storage: &dyn Storage,
87    to_contract: impl ToString,
88    to_version: Version,
89) -> Result<(), AbstractError> {
90    let ContractVersion {
91        version: from_version,
92        contract,
93    } = get_contract_version(storage)?;
94
95    let to_contract = to_contract.to_string();
96
97    // Must be the same contract
98    ensure_eq!(
99        contract,
100        to_contract,
101        AbstractError::ContractNameMismatch {
102            from: contract,
103            to: to_contract,
104        }
105    );
106
107    let from_version = from_version.parse().unwrap();
108
109    // Must be a version upgrade
110    ensure!(
111        to_version > from_version,
112        AbstractError::CannotDowngradeContract {
113            contract,
114            from: from_version,
115            to: to_version,
116        }
117    );
118    // Must be 1 major or 1 minor version bump, not more
119    // Patches we ignore
120    let major_diff = to_version.major.checked_sub(from_version.major);
121    let minor_diff = to_version.minor.checked_sub(from_version.minor);
122    let no_skips = match (major_diff, minor_diff) {
123        // 1) major upgrade - minor should stay the same (1.0.0 -> 2.0.0)
124        // 2) major upgrade - minor sub overflowed (0.1.0 -> 1.0.0)
125        (Some(1), _) => true,
126        // minor upgrade - major should stay the same (1.0.0 -> 1.1.0)
127        (Some(0), Some(1)) => true,
128        // patch upgrade - minor and major stays the same (1.0.0 -> 1.0.1)
129        (Some(0), Some(0)) => true,
130        _ => false,
131    };
132    ensure!(
133        no_skips,
134        AbstractError::CannotSkipVersion {
135            contract,
136            from: from_version,
137            to: to_version,
138        }
139    );
140    Ok(())
141}
142
143/// Assert that the new version is greater than the stored version.
144pub fn assert_cw_contract_upgrade(
145    storage: &dyn Storage,
146    to_contract: impl ToString,
147    to_version: semver::Version,
148) -> Result<(), AbstractError> {
149    assert_contract_upgrade(
150        storage,
151        to_contract,
152        to_version.to_string().parse().unwrap(),
153    )
154}
155
156/// Migrate the module data to the new state.
157/// If there was no moduleData stored, it will be set to the given values with an empty dependency array.
158/// If the metadata is `None`, the old metadata will be kept.
159/// If the metadata is `Some`, the old metadata will be overwritten.
160pub fn migrate_module_data(
161    store: &mut dyn Storage,
162    name: &str,
163    version: &str,
164    metadata: Option<String>,
165) -> StdResult<()> {
166    let old_module_data = MODULE.may_load(store)?;
167    let val = old_module_data.map_or(
168        ModuleData {
169            module: name.into(),
170            version: version.into(),
171            dependencies: vec![],
172            metadata: None,
173        },
174        |data| ModuleData {
175            module: name.into(),
176            version: version.into(),
177            dependencies: data.dependencies,
178            metadata: metadata.or(data.metadata),
179        },
180    );
181
182    MODULE.save(store, &val).map_err(Into::into)
183}
184
185/// This will make a raw_query to another module to determine the current version it
186/// claims to be. This should not be trusted, but could be used as a quick filter
187/// if the other module exists and claims to be a cw20-base module for example.
188/// (Note: you usually want to require *interfaces* not *implementations* of the
189/// contracts you compose with, so be careful of overuse)
190pub fn query_module_data<Q: Querier, T: Into<String>>(
191    querier: &Q,
192    contract_addr: T,
193) -> StdResult<ModuleData> {
194    let req = QueryRequest::Wasm(WasmQuery::Raw {
195        contract_addr: contract_addr.into(),
196        key: MODULE.as_slice().into(),
197    });
198    QuerierWrapper::<Empty>::new(querier)
199        .query(&req)
200        .map_err(Into::into)
201}
202
203#[cfg(test)]
204mod tests {
205    use cosmwasm_std::testing::MockStorage;
206
207    use super::*;
208
209    #[coverage_helper::test]
210    fn set_works() {
211        let mut store = MockStorage::new();
212
213        // set and get
214        let contract_name = "crate:cw20-base";
215        let contract_version = "0.2.0";
216        let metadata = Some("https://example.com");
217        const REQUIREMENT: [&str; 1] = [">1"];
218
219        const DEPENDENCIES: &[StaticDependency; 1] = &[StaticDependency {
220            id: "abstact::dex",
221            version_req: &REQUIREMENT,
222        }];
223        set_module_data(
224            &mut store,
225            contract_name,
226            contract_version,
227            DEPENDENCIES,
228            metadata,
229        )
230        .unwrap();
231
232        let loaded = MODULE.load(&store).unwrap();
233        let expected = ModuleData {
234            module: contract_name.to_string(),
235            version: contract_version.to_string(),
236            dependencies: DEPENDENCIES.iter().map(Into::into).collect(),
237            metadata: metadata.map(Into::into),
238        };
239        assert_eq!(expected, loaded);
240    }
241
242    #[coverage_helper::test]
243    fn module_upgrade() {
244        let mut store = MockStorage::new();
245        let contract_name = "abstract:account";
246        let contract_version = "0.19.2";
247        cw2::CONTRACT
248            .save(
249                &mut store,
250                &ContractVersion {
251                    contract: contract_name.to_owned(),
252                    version: contract_version.to_owned(),
253                },
254            )
255            .unwrap();
256
257        // Patch upgrade
258        let to_version = "0.19.3".parse().unwrap();
259        let res = assert_contract_upgrade(&store, contract_name, to_version);
260        assert!(res.is_ok());
261
262        // Minor upgrade
263        let to_version = "0.20.0".parse().unwrap();
264        let res = assert_contract_upgrade(&store, contract_name, to_version);
265        assert!(res.is_ok());
266
267        // Minor with patch upgrade
268        let to_version = "0.20.1".parse().unwrap();
269        let res = assert_contract_upgrade(&store, contract_name, to_version);
270        assert!(res.is_ok());
271
272        // Major upgrade
273        let to_version = "1.0.0".parse().unwrap();
274        let res = assert_contract_upgrade(&store, contract_name, to_version);
275        assert!(res.is_ok());
276    }
277
278    #[coverage_helper::test]
279    fn module_upgrade_err() {
280        let mut store = MockStorage::new();
281        let contract_name = "abstract:account";
282        let contract_version = "0.19.2";
283        cw2::CONTRACT
284            .save(
285                &mut store,
286                &ContractVersion {
287                    contract: contract_name.to_owned(),
288                    version: contract_version.to_owned(),
289                },
290            )
291            .unwrap();
292
293        // Downgrade
294        let to_version: Version = "0.19.1".parse().unwrap();
295        let err = assert_contract_upgrade(&store, contract_name, to_version.clone()).unwrap_err();
296        assert_eq!(
297            err,
298            AbstractError::CannotDowngradeContract {
299                contract: contract_name.to_string(),
300                from: contract_version.parse().unwrap(),
301                to: to_version
302            }
303        );
304
305        // Minor upgrade
306        let to_version: Version = "0.21.0".parse().unwrap();
307        let err = assert_contract_upgrade(&store, contract_name, to_version.clone()).unwrap_err();
308        assert_eq!(
309            err,
310            AbstractError::CannotSkipVersion {
311                contract: contract_name.to_string(),
312                from: contract_version.parse().unwrap(),
313                to: to_version
314            }
315        );
316
317        // Major upgrade
318        let to_version: Version = "2.0.0".parse().unwrap();
319        let err = assert_contract_upgrade(&store, contract_name, to_version.clone()).unwrap_err();
320        assert_eq!(
321            err,
322            AbstractError::CannotSkipVersion {
323                contract: contract_name.to_string(),
324                from: contract_version.parse().unwrap(),
325                to: to_version
326            }
327        );
328    }
329}