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