From 3a6093936520e9a1559423b50aa911ed9b836b3c Mon Sep 17 00:00:00 2001 From: Tomasz Kulik Date: Thu, 5 Sep 2024 11:40:21 +0200 Subject: [PATCH] Conditional migrate call with extra MigrateInfo argument after contract update (#2212) --- .circleci/config.yml | 1 + CHANGELOG.md | 6 + contracts/README.md | 2 +- contracts/hackatom/Cargo.toml | 2 +- contracts/hackatom/src/contract.rs | 26 +++- contracts/hackatom/src/errors.rs | 3 + contracts/hackatom/tests/integration.rs | 12 +- packages/check/src/main.rs | 2 +- packages/derive/src/lib.rs | 193 ++++++++++++++++++------ packages/std/src/exports.rs | 58 ++++++- packages/std/src/lib.rs | 4 +- packages/std/src/types.rs | 16 ++ packages/vm/src/calls.rs | 104 ++++++++++++- packages/vm/src/environment.rs | 4 + packages/vm/src/errors/vm_error.rs | 19 ++- packages/vm/src/lib.rs | 5 +- packages/vm/src/testing/calls.rs | 61 +++++--- packages/vm/src/testing/mod.rs | 2 +- 18 files changed, 433 insertions(+), 87 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index cdbddef9b5..1cc5524d2e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -637,6 +637,7 @@ jobs: - cargocache-v2-contract_hackatom-rust:1.74-{{ checksum "Cargo.lock" }} - check_contract: min_version: "1.4" + skip_cosmwasm_check: true - save_cache: paths: - /usr/local/cargo/registry diff --git a/CHANGELOG.md b/CHANGELOG.md index 104f1f5b55..ab0434ec7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,10 +21,16 @@ and this project adheres to validation. ([#2220]) - cosmwasm-check: Add `--wasm-limits` flag to supply configured limits for static validation. ([#2220]) +- cosmwasm-std: Add `migrate_with_info` call implementation for the extended + `migrate` entrypoint function ([#2212]) +- cosmwasm-vm: Export a new `migrate_with_info` function ([#2212]) +- cosmwasm-derive: Add support for migrate method with + `migrate_info: MigrateInfo` argument. ([#2212]) [#2118]: https://github.com/CosmWasm/cosmwasm/pull/2118 [#2196]: https://github.com/CosmWasm/cosmwasm/pull/2196 [#2220]: https://github.com/CosmWasm/cosmwasm/pull/2220 +[#2212]: https://github.com/CosmWasm/cosmwasm/pull/2212 ### Changed diff --git a/contracts/README.md b/contracts/README.md index 7f6bc2688c..9600b5dbe5 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -18,7 +18,7 @@ Introducing the development contracts in the order they were created. [mask contract](https://medium.com/cosmwasm/introducing-the-mask-41d11e51bccf), which allows the user to send messages to the contract which are then emitted with the contract as the sender. It later got support to handle sub messages - and replys ([#796](https://github.com/CosmWasm/cosmwasm/pull/796)). + and replies ([#796](https://github.com/CosmWasm/cosmwasm/pull/796)). 4. **staking** is a staking derivatives example showing how the contract itself can be a delegator. 5. **burner** shows how contract migrations work, which were added in CosmWasm diff --git a/contracts/hackatom/Cargo.toml b/contracts/hackatom/Cargo.toml index 6e6f47a129..fd77d71fa5 100644 --- a/contracts/hackatom/Cargo.toml +++ b/contracts/hackatom/Cargo.toml @@ -30,7 +30,7 @@ cranelift = ["cosmwasm-vm/cranelift"] [dependencies] cosmwasm-schema = { path = "../../packages/schema" } -cosmwasm-std = { path = "../../packages/std", default-features = false, features = ["std", "abort"] } +cosmwasm-std = { path = "../../packages/std", default-features = false, features = ["std", "abort", "cosmwasm_2_2"] } schemars = "0.8.12" serde = { version = "1.0.103", default-features = false, features = ["derive"] } sha2 = "0.10" diff --git a/contracts/hackatom/src/contract.rs b/contracts/hackatom/src/contract.rs index ad21644810..f2e03c2720 100644 --- a/contracts/hackatom/src/contract.rs +++ b/contracts/hackatom/src/contract.rs @@ -2,8 +2,8 @@ use sha2::{Digest, Sha256}; use cosmwasm_std::{ entry_point, from_json, to_json_binary, to_json_vec, Addr, AllBalanceResponse, Api, BankMsg, - BankQuery, CanonicalAddr, Deps, DepsMut, Env, Event, MessageInfo, QueryRequest, QueryResponse, - Response, StdError, StdResult, WasmMsg, WasmQuery, + BankQuery, CanonicalAddr, Deps, DepsMut, Env, Event, MessageInfo, MigrateInfo, QueryRequest, + QueryResponse, Response, StdError, StdResult, WasmMsg, WasmQuery, }; use crate::errors::HackError; @@ -35,9 +35,21 @@ pub fn instantiate( Ok(Response::new().add_attribute("Let the", "hacking begin")) } +const CONTRACT_MIGRATE_VERSION: u64 = 420; + #[entry_point] -#[migrate_version(42)] -pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { +#[migrate_version(CONTRACT_MIGRATE_VERSION)] +pub fn migrate( + deps: DepsMut, + _env: Env, + msg: MigrateMsg, + migrate_info: MigrateInfo, +) -> Result { + if let Some(old_version) = migrate_info.old_migrate_version { + if CONTRACT_MIGRATE_VERSION <= old_version { + return Err(HackError::Downgrade); + } + } let data = deps .storage .get(CONFIG_KEY) @@ -379,7 +391,11 @@ mod tests { let msg = MigrateMsg { verifier: new_verifier.clone(), }; - let res = migrate(deps.as_mut(), mock_env(), msg).unwrap(); + let migrate_info = MigrateInfo { + sender: creator, + old_migrate_version: None, + }; + let res = migrate(deps.as_mut(), mock_env(), msg, migrate_info).unwrap(); assert_eq!(0, res.messages.len()); // check it is 'someone else' diff --git a/contracts/hackatom/src/errors.rs b/contracts/hackatom/src/errors.rs index 7b46b00b63..a72d292460 100644 --- a/contracts/hackatom/src/errors.rs +++ b/contracts/hackatom/src/errors.rs @@ -9,4 +9,7 @@ pub enum HackError { // this is whatever we want #[error("Unauthorized")] Unauthorized {}, + // this is whatever we want + #[error("Downgrade is not supported")] + Downgrade, } diff --git a/contracts/hackatom/tests/integration.rs b/contracts/hackatom/tests/integration.rs index ef192d7366..9459c08c74 100644 --- a/contracts/hackatom/tests/integration.rs +++ b/contracts/hackatom/tests/integration.rs @@ -19,12 +19,12 @@ use cosmwasm_std::{ assert_approx_eq, coins, from_json, to_json_vec, Addr, AllBalanceResponse, BankMsg, Binary, - ContractResult, Empty, Response, SubMsg, + ContractResult, Empty, MigrateInfo, Response, SubMsg, }; use cosmwasm_vm::{ call_execute, from_slice, testing::{ - execute, instantiate, migrate, mock_env, mock_info, mock_instance, + execute, instantiate, migrate_with_info, mock_env, mock_info, mock_instance, mock_instance_with_balances, query, sudo, test_io, MockApi, MOCK_CONTRACT_ADDR, }, Storage, VmError, @@ -53,7 +53,7 @@ fn make_init_msg(api: &MockApi) -> (InstantiateMsg, String) { #[test] fn proper_initialization() { let mut deps = mock_instance(WASM, &[]); - assert_eq!(deps.required_capabilities().len(), 0); + assert_eq!(deps.required_capabilities().len(), 7); let verifier = deps.api().addr_make("verifies"); let beneficiary = deps.api().addr_make("benefits"); @@ -142,7 +142,11 @@ fn migrate_verifier() { let msg = MigrateMsg { verifier: someone_else.clone(), }; - let res: Response = migrate(&mut deps, mock_env(), msg).unwrap(); + let migrate_info = MigrateInfo { + sender: Addr::unchecked(creator), + old_migrate_version: None, + }; + let res: Response = migrate_with_info(&mut deps, mock_env(), msg, migrate_info).unwrap(); assert_eq!(0, res.messages.len()); // check it is 'someone else' diff --git a/packages/check/src/main.rs b/packages/check/src/main.rs index 06634a0b35..559fbc3642 100644 --- a/packages/check/src/main.rs +++ b/packages/check/src/main.rs @@ -13,7 +13,7 @@ use cosmwasm_vm::internals::{check_wasm, compile, make_compiling_engine, LogOutp use cosmwasm_vm::{capabilities_from_csv, WasmLimits}; const DEFAULT_AVAILABLE_CAPABILITIES: &str = - "iterator,staking,stargate,cosmwasm_1_1,cosmwasm_1_2,cosmwasm_1_3,cosmwasm_1_4,cosmwasm_2_0,cosmwasm_2_1"; + "iterator,staking,stargate,cosmwasm_1_1,cosmwasm_1_2,cosmwasm_1_3,cosmwasm_1_4,cosmwasm_2_0,cosmwasm_2_1,cosmwasm_2_2"; pub fn main() { let matches = Command::new("Contract checking") diff --git a/packages/derive/src/lib.rs b/packages/derive/src/lib.rs index 35da7c97db..253b10f945 100644 --- a/packages/derive/src/lib.rs +++ b/packages/derive/src/lib.rs @@ -100,14 +100,33 @@ impl Parse for Options { /// /// ``` /// # use cosmwasm_std::{ -/// # DepsMut, entry_point, Env, +/// # DepsMut, entry_point, Env, MigrateInfo, /// # Response, StdResult, /// # }; /// # /// # type MigrateMsg = (); /// #[entry_point] /// #[migrate_version(2)] -/// pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> StdResult { +/// pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg, migrate_info: MigrateInfo) -> StdResult { +/// todo!(); +/// } +/// ``` +/// +/// It is also possible to assign the migrate version number to +/// a given constant name: +/// +/// ``` +/// # use cosmwasm_std::{ +/// # DepsMut, entry_point, Env, MigrateInfo, +/// # Response, StdResult, +/// # }; +/// # +/// # type MigrateMsg = (); +/// const CONTRACT_VERSION: u64 = 66; +/// +/// #[entry_point] +/// #[migrate_version(CONTRACT_VERSION)] +/// pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg, migrate_info: MigrateInfo) -> StdResult { /// todo!(); /// } /// ``` @@ -135,29 +154,50 @@ fn expand_attributes(func: &mut ItemFn) -> syn::Result { )); } - let version: syn::LitInt = attribute.parse_args()?; - // Enforce that the version is a valid u64 and non-zero - if version.base10_parse::()? == 0 { + let version: syn::Expr = attribute.parse_args()?; + if !(matches!(version, syn::Expr::Lit(_)) || matches!(version, syn::Expr::Path(_))) { return Err(syn::Error::new_spanned( - version, - "please start versioning with 1", + &attribute, + "Expected `u64` or `path::to::constant` in the migrate_version attribute", )); } - let version = version.base10_digits(); - let n = version.len(); - let version = proc_macro2::Literal::byte_string(version.as_bytes()); - stream = quote! { #stream - #[allow(unused)] - #[doc(hidden)] - #[cfg(target_arch = "wasm32")] - #[link_section = "cw_migrate_version"] - /// This is an internal constant exported as a custom section denoting the contract migrate version. - /// The format and even the existence of this value is an implementation detail, DO NOT RELY ON THIS! - static __CW_MIGRATE_VERSION: [u8; #n] = *#version; + const _: () = { + #[allow(unused)] + #[doc(hidden)] + #[cfg(target_arch = "wasm32")] + #[link_section = "cw_migrate_version"] + /// This is an internal constant exported as a custom section denoting the contract migrate version. + /// The format and even the existence of this value is an implementation detail, DO NOT RELY ON THIS! + static __CW_MIGRATE_VERSION: [u8; version_size(#version)] = stringify_version(#version); + + #[allow(unused)] + #[doc(hidden)] + const fn stringify_version(mut version: u64) -> [u8; N] { + let mut result: [u8; N] = [0; N]; + let mut index = N; + while index > 0 { + let digit: u8 = (version%10) as u8; + result[index-1] = digit + b'0'; + version /= 10; + index -= 1; + } + result + } + + #[allow(unused)] + #[doc(hidden)] + const fn version_size(version: u64) -> usize { + if version > 0 { + (version.ilog10()+1) as usize + } else { + panic!("Contract migrate version should be greater than 0.") + } + } + }; }; } @@ -174,7 +214,13 @@ fn entry_point_impl(attr: TokenStream, item: TokenStream) -> TokenStream { let args = function.sig.inputs.len().saturating_sub(1); let fn_name = &function.sig.ident; let wasm_export = format_ident!("__wasm_export_{fn_name}"); - let do_call = format_ident!("do_{fn_name}"); + + // Migrate entry point can take 2 or 3 arguments + let do_call = if fn_name == "migrate" && args == 3 { + format_ident!("do_migrate_with_info") + } else { + format_ident!("do_{fn_name}") + }; let decl_args = (0..args).map(|item| format_ident!("ptr_{item}")); let call_args = decl_args.clone(); @@ -202,60 +248,87 @@ mod test { use crate::entry_point_impl; #[test] - fn contract_state_zero_not_allowed() { + fn contract_migrate_version_on_non_migrate() { let code = quote! { - #[migrate_version(0)] - fn migrate() -> Response { + #[migrate_version(42)] + fn anything_else() -> Response { // Logic here } }; let actual = entry_point_impl(TokenStream::new(), code); let expected = quote! { - ::core::compile_error! { "please start versioning with 1" } + ::core::compile_error! { "you only want to add this attribute to your migrate function" } }; assert_eq!(actual.to_string(), expected.to_string()); } #[test] - fn contract_migrate_version_on_non_migrate() { + fn contract_migrate_version_expansion() { let code = quote! { - #[migrate_version(42)] - fn anything_else() -> Response { + #[migrate_version(2)] + fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Response { // Logic here } }; let actual = entry_point_impl(TokenStream::new(), code); let expected = quote! { - ::core::compile_error! { "you only want to add this attribute to your migrate function" } - }; + const _: () = { + #[allow(unused)] + #[doc(hidden)] + #[cfg(target_arch = "wasm32")] + #[link_section = "cw_migrate_version"] + /// This is an internal constant exported as a custom section denoting the contract migrate version. + /// The format and even the existence of this value is an implementation detail, DO NOT RELY ON THIS! + static __CW_MIGRATE_VERSION: [u8; version_size(2)] = stringify_version(2); + + #[allow(unused)] + #[doc(hidden)] + const fn stringify_version(mut version: u64) -> [u8; N] { + let mut result: [u8; N] = [0; N]; + let mut index = N; + while index > 0 { + let digit: u8 = (version%10) as u8; + result[index-1] = digit + b'0'; + version /= 10; + index -= 1; + } + result + } - assert_eq!(actual.to_string(), expected.to_string()); - } + #[allow(unused)] + #[doc(hidden)] + const fn version_size(version: u64) -> usize { + if version > 0 { + (version.ilog10()+1) as usize + } else { + panic!("Contract migrate version should be greater than 0.") + } + } + }; - #[test] - fn contract_migrate_version_in_u64() { - let code = quote! { - #[migrate_version(0xDEAD_BEEF_FFFF_DEAD_2BAD)] fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Response { // Logic here } - }; - let actual = entry_point_impl(TokenStream::new(), code); - let expected = quote! { - ::core::compile_error! { "number too large to fit in target type" } + #[cfg(target_arch = "wasm32")] + mod __wasm_export_migrate { + #[no_mangle] + extern "C" fn migrate(ptr_0: u32, ptr_1: u32) -> u32 { + ::cosmwasm_std::do_migrate(&super::migrate, ptr_0, ptr_1) + } + } }; assert_eq!(actual.to_string(), expected.to_string()); } #[test] - fn contract_migrate_version_expansion() { + fn contract_migrate_version_with_const_expansion() { let code = quote! { - #[migrate_version(2)] + #[migrate_version(CONTRACT_VERSION)] fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Response { // Logic here } @@ -263,13 +336,39 @@ mod test { let actual = entry_point_impl(TokenStream::new(), code); let expected = quote! { - #[allow(unused)] - #[doc(hidden)] - #[cfg(target_arch = "wasm32")] - #[link_section = "cw_migrate_version"] - /// This is an internal constant exported as a custom section denoting the contract migrate version. - /// The format and even the existence of this value is an implementation detail, DO NOT RELY ON THIS! - static __CW_MIGRATE_VERSION: [u8; 1usize] = *b"2"; + const _: () = { + #[allow(unused)] + #[doc(hidden)] + #[cfg(target_arch = "wasm32")] + #[link_section = "cw_migrate_version"] + /// This is an internal constant exported as a custom section denoting the contract migrate version. + /// The format and even the existence of this value is an implementation detail, DO NOT RELY ON THIS! + static __CW_MIGRATE_VERSION: [u8; version_size(CONTRACT_VERSION)] = stringify_version(CONTRACT_VERSION); + + #[allow(unused)] + #[doc(hidden)] + const fn stringify_version(mut version: u64) -> [u8; N] { + let mut result: [u8; N] = [0; N]; + let mut index = N; + while index > 0 { + let digit: u8 = (version%10) as u8; + result[index-1] = digit + b'0'; + version /= 10; + index -= 1; + } + result + } + + #[allow(unused)] + #[doc(hidden)] + const fn version_size(version: u64) -> usize { + if version > 0 { + (version.ilog10()+1) as usize + } else { + panic!("Contract migrate version should be greater than 0.") + } + } + }; fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Response { // Logic here diff --git a/packages/std/src/exports.rs b/packages/std/src/exports.rs index c176a47bd1..b8057298e5 100644 --- a/packages/std/src/exports.rs +++ b/packages/std/src/exports.rs @@ -28,7 +28,7 @@ use crate::query::CustomQuery; use crate::results::{ContractResult, QueryResponse, Reply, Response}; use crate::serde::{from_json, to_json_vec}; use crate::types::Env; -use crate::{CustomMsg, Deps, DepsMut, MessageInfo}; +use crate::{CustomMsg, Deps, DepsMut, MessageInfo, MigrateInfo}; // These functions are used as markers for the chain to know which features this contract requires. // If the chain does not support all the required features, it will reject storing the contract. @@ -198,6 +198,38 @@ where Region::from_vec(v).to_heap_ptr() as u32 } +/// do_migrate_with_info should be wrapped in an external "C" export, +/// containing a contract-specific function as arg +/// +/// - `Q`: custom query type (see QueryRequest) +/// - `M`: message type for request +/// - `C`: custom response message type (see CosmosMsg) +/// - `E`: error type for responses +#[cfg(feature = "cosmwasm_2_2")] +pub fn do_migrate_with_info( + migrate_with_info_fn: &dyn Fn(DepsMut, Env, M, MigrateInfo) -> Result, E>, + env_ptr: u32, + msg_ptr: u32, + migrate_info_ptr: u32, +) -> u32 +where + Q: CustomQuery, + M: DeserializeOwned, + C: CustomMsg, + E: ToString, +{ + #[cfg(feature = "abort")] + install_panic_handler(); + let res = _do_migrate_with_info( + migrate_with_info_fn, + env_ptr as *mut Region, + msg_ptr as *mut Region, + migrate_info_ptr as *mut Region, + ); + let v = to_json_vec(&res).unwrap(); + Region::from_vec(v).to_heap_ptr() as u32 +} + /// do_sudo should be wrapped in an external "C" export, containing a contract-specific function as arg /// /// - `Q`: custom query type (see QueryRequest) @@ -570,6 +602,30 @@ where migrate_fn(deps.as_mut(), env, msg).into() } +fn _do_migrate_with_info( + migrate_with_info_fn: &dyn Fn(DepsMut, Env, M, MigrateInfo) -> Result, E>, + env_ptr: *mut Region, + msg_ptr: *mut Region, + migrate_info_ptr: *mut Region, +) -> ContractResult> +where + Q: CustomQuery, + M: DeserializeOwned, + C: CustomMsg, + E: ToString, +{ + let env: Vec = unsafe { Region::from_heap_ptr(env_ptr).into_vec() }; + let msg: Vec = unsafe { Region::from_heap_ptr(msg_ptr).into_vec() }; + let migrate_info = unsafe { Region::from_heap_ptr(migrate_info_ptr).into_vec() }; + + let env: Env = try_into_contract_result!(from_json(env)); + let msg: M = try_into_contract_result!(from_json(msg)); + let migrate_info: MigrateInfo = try_into_contract_result!(from_json(migrate_info)); + + let mut deps = make_dependencies(); + migrate_with_info_fn(deps.as_mut(), env, msg, migrate_info).into() +} + fn _do_sudo( sudo_fn: &dyn Fn(DepsMut, Env, M) -> Result, E>, env_ptr: *mut Region, diff --git a/packages/std/src/lib.rs b/packages/std/src/lib.rs index 962409d9e2..f683954cfe 100644 --- a/packages/std/src/lib.rs +++ b/packages/std/src/lib.rs @@ -116,7 +116,7 @@ pub use crate::stdack::StdAck; pub use crate::storage::MemoryStorage; pub use crate::timestamp::Timestamp; pub use crate::traits::{Api, HashFunction, Querier, QuerierResult, QuerierWrapper, Storage}; -pub use crate::types::{BlockInfo, ContractInfo, Env, MessageInfo, TransactionInfo}; +pub use crate::types::{BlockInfo, ContractInfo, Env, MessageInfo, MigrateInfo, TransactionInfo}; // Exposed in wasm build only @@ -127,6 +127,8 @@ mod imports; #[cfg(target_arch = "wasm32")] mod memory; // Used by exports and imports only. This assumes pointers are 32 bit long, which makes it untestable on dev machines. +#[cfg(all(feature = "cosmwasm_2_2", target_arch = "wasm32"))] +pub use crate::exports::do_migrate_with_info; #[cfg(target_arch = "wasm32")] pub use crate::exports::{ do_execute, do_ibc_destination_callback, do_ibc_source_callback, do_instantiate, do_migrate, diff --git a/packages/std/src/types.rs b/packages/std/src/types.rs index f85d42e8a0..ab8c14040f 100644 --- a/packages/std/src/types.rs +++ b/packages/std/src/types.rs @@ -109,3 +109,19 @@ pub struct MessageInfo { pub struct ContractInfo { pub address: Addr, } + +/// The structure contains additional information related to the +/// contract's migration procedure - the sender address and +/// the contract's migrate version currently stored on the blockchain. +/// The `old_migrate_version` is optional, since there is no guarantee +/// that the currently stored contract's binary contains that information. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct MigrateInfo { + /// Address of the sender. + /// + /// This is the `sender` field from [`MsgMigrateContract`](https://github.com/CosmWasm/wasmd/blob/v0.53.0/proto/cosmwasm/wasm/v1/tx.proto#L217-L233). + pub sender: Addr, + /// Migrate version of the previous contract. It's optional, since + /// adding the version number to the binary is not a mandatory feature. + pub old_migrate_version: Option, +} diff --git a/packages/vm/src/calls.rs b/packages/vm/src/calls.rs index 96df03672c..5f6af1de19 100644 --- a/packages/vm/src/calls.rs +++ b/packages/vm/src/calls.rs @@ -3,7 +3,7 @@ use wasmer::Value; use cosmwasm_std::{ ContractResult, CustomMsg, Env, IbcBasicResponse, IbcDestinationCallbackMsg, - IbcSourceCallbackMsg, MessageInfo, QueryResponse, Reply, Response, + IbcSourceCallbackMsg, MessageInfo, MigrateInfo, QueryResponse, Reply, Response, }; #[cfg(feature = "stargate")] use cosmwasm_std::{ @@ -163,6 +163,26 @@ where Ok(result) } +pub fn call_migrate_with_info( + instance: &mut Instance, + env: &Env, + msg: &[u8], + migrate_info: &MigrateInfo, +) -> VmResult>> +where + A: BackendApi + 'static, + S: Storage + 'static, + Q: Querier + 'static, + U: DeserializeOwned + CustomMsg, +{ + let env = to_vec(env)?; + let migrate_info = to_vec(migrate_info)?; + let data = call_migrate_with_info_raw(instance, &env, msg, &migrate_info)?; + let result: ContractResult> = + from_slice(&data, deserialization_limits::RESULT_MIGRATE)?; + Ok(result) +} + pub fn call_sudo( instance: &mut Instance, env: &Env, @@ -441,6 +461,47 @@ where ) } +/// Calls Wasm export "migrate" and returns raw data from the contract. +/// The result is length limited to prevent abuse but otherwise unchecked. +/// The difference between this function and [call_migrate_raw] is the +/// additional argument - `migrate_info`. It contains additional data +/// related to the contract's migration procedure. +/// +/// It is safe to call this method instead of [call_migrate_raw] even +/// if a contract contains the migrate entrypoint without `migrate_info`. +/// In such case this structure is omitted. +pub fn call_migrate_with_info_raw( + instance: &mut Instance, + env: &[u8], + msg: &[u8], + migrate_info: &[u8], +) -> VmResult> +where + A: BackendApi + 'static, + S: Storage + 'static, + Q: Querier + 'static, +{ + instance.set_storage_readonly(false); + call_raw( + instance, + "migrate", + &[env, msg, migrate_info], + read_limits::RESULT_MIGRATE, + ) + .or_else(|err| { + if matches!(err, VmError::FunctionArityMismatch { .. }) { + call_raw( + instance, + "migrate", + &[env, msg], + read_limits::RESULT_MIGRATE, + ) + } else { + Err(err) + } + }) +} + /// Calls Wasm export "sudo" and returns raw data from the contract. /// The result is length limited to prevent abuse but otherwise unchecked. pub fn call_sudo_raw( @@ -680,7 +741,7 @@ mod tests { use crate::testing::{ mock_env, mock_info, mock_instance, mock_instance_with_options, MockInstanceOptions, }; - use cosmwasm_std::{coins, from_json, to_json_string, Empty}; + use cosmwasm_std::{coins, from_json, to_json_string, Addr, Empty}; use sha2::{Digest, Sha256}; static CONTRACT: &[u8] = include_bytes!("../testdata/hackatom.wasm"); @@ -840,6 +901,45 @@ mod tests { ); } + #[test] + fn call_migrate_with_info_works() { + let mut instance = mock_instance(CONTRACT, &[]); + + // init + let info = mock_info(&instance.api().addr_make("creator"), &coins(1000, "earth")); + let verifier = instance.api().addr_make("verifies"); + let beneficiary = instance.api().addr_make("benefits"); + let msg = format!(r#"{{"verifier": "{verifier}", "beneficiary": "{beneficiary}"}}"#); + call_instantiate::<_, _, _, Empty>(&mut instance, &mock_env(), &info, msg.as_bytes()) + .unwrap() + .unwrap(); + + // change the verifier via migrate + let someone_else = instance.api().addr_make("someone else"); + let msg = format!(r#"{{"verifier": "{someone_else}"}}"#); + let migrate_info = MigrateInfo { + sender: Addr::unchecked(someone_else.clone()), + old_migrate_version: Some(33), + }; + let _res = call_migrate_with_info::<_, _, _, Empty>( + &mut instance, + &mock_env(), + msg.as_bytes(), + &migrate_info, + ) + .unwrap() + .unwrap(); + + // query the new_verifier with verifier + let msg = br#"{"verifier":{}}"#; + let contract_result = call_query(&mut instance, &mock_env(), msg).unwrap(); + let query_response = contract_result.unwrap(); + assert_eq!( + query_response, + format!(r#"{{"verifier":"{}"}}"#, someone_else).as_bytes(), + ); + } + #[test] fn call_query_works() { let mut instance = mock_instance(CONTRACT, &[]); diff --git a/packages/vm/src/environment.rs b/packages/vm/src/environment.rs index d3e0a5b538..2a5df1cec0 100644 --- a/packages/vm/src/environment.rs +++ b/packages/vm/src/environment.rs @@ -272,6 +272,10 @@ impl Environment { let func = instance.exports.get_function(name)?; Ok(func.clone()) })?; + let function_arity = func.param_arity(store); + if args.len() != function_arity { + return Err(VmError::function_arity_mismatch(function_arity)); + }; self.increment_call_depth()?; let res = func.call(store, args).map_err(|runtime_err| -> VmError { self.with_wasmer_instance::<_, Never>(|instance| { diff --git a/packages/vm/src/errors/vm_error.rs b/packages/vm/src/errors/vm_error.rs index 8987aaa199..5a6260418a 100644 --- a/packages/vm/src/errors/vm_error.rs +++ b/packages/vm/src/errors/vm_error.rs @@ -86,6 +86,14 @@ pub enum VmError { WriteAccessDenied { backtrace: BT }, #[error("Maximum call depth exceeded.")] MaxCallDepthExceeded { backtrace: BT }, + #[error( + "The called function args arity does not match. The contract's method arity: {}", + contract_method_arity + )] + FunctionArityMismatch { + contract_method_arity: usize, + backtrace: BT, + }, } impl VmError { @@ -242,6 +250,13 @@ impl VmError { backtrace: BT::capture(), } } + + pub(crate) fn function_arity_mismatch(contract_method_arity: usize) -> Self { + VmError::FunctionArityMismatch { + contract_method_arity, + backtrace: BT::capture(), + } + } } impl_from_err!(CommunicationError, VmError, VmError::CommunicationErr); @@ -297,7 +312,7 @@ impl From for VmError { let message = format!("RuntimeError: {}", original.message()); debug_assert!( original.to_string().starts_with(&message), - "The error message we created is not a prefix of the error message from Wasmer. Our message: '{}'. Wasmer messsage: '{}'", + "The error message we created is not a prefix of the error message from Wasmer. Our message: '{}'. Wasmer message: '{}'", &message, original ); @@ -379,7 +394,7 @@ mod tests { } #[test] - fn cyrpto_err_works() { + fn crypto_err_works() { let error = VmError::crypto_err(CryptoError::generic_err("something went wrong")); match error { VmError::CryptoErr { diff --git a/packages/vm/src/lib.rs b/packages/vm/src/lib.rs index 6efb4a6616..e67ea78c8f 100644 --- a/packages/vm/src/lib.rs +++ b/packages/vm/src/lib.rs @@ -28,8 +28,9 @@ pub use crate::cache::{AnalysisReport, Cache, Metrics, PerModuleMetrics, PinnedM pub use crate::calls::{ call_execute, call_execute_raw, call_ibc_destination_callback, call_ibc_destination_callback_raw, call_ibc_source_callback, call_ibc_source_callback_raw, - call_instantiate, call_instantiate_raw, call_migrate, call_migrate_raw, call_query, - call_query_raw, call_reply, call_reply_raw, call_sudo, call_sudo_raw, + call_instantiate, call_instantiate_raw, call_migrate, call_migrate_raw, call_migrate_with_info, + call_migrate_with_info_raw, call_query, call_query_raw, call_reply, call_reply_raw, call_sudo, + call_sudo_raw, }; #[cfg(feature = "stargate")] pub use crate::calls::{ diff --git a/packages/vm/src/testing/calls.rs b/packages/vm/src/testing/calls.rs index 4c11640ed9..fd09e6e15b 100644 --- a/packages/vm/src/testing/calls.rs +++ b/packages/vm/src/testing/calls.rs @@ -4,7 +4,9 @@ use schemars::JsonSchema; use serde::{de::DeserializeOwned, Serialize}; -use cosmwasm_std::{ContractResult, CustomMsg, Env, MessageInfo, QueryResponse, Reply, Response}; +use cosmwasm_std::{ + ContractResult, CustomMsg, Env, MessageInfo, MigrateInfo, QueryResponse, Reply, Response, +}; #[cfg(feature = "stargate")] use cosmwasm_std::{ Ibc3ChannelOpenResponse, IbcBasicResponse, IbcChannelCloseMsg, IbcChannelConnectMsg, @@ -13,7 +15,8 @@ use cosmwasm_std::{ }; use crate::calls::{ - call_execute, call_instantiate, call_migrate, call_query, call_reply, call_sudo, + call_execute, call_instantiate, call_migrate, call_migrate_with_info, call_query, call_reply, + call_sudo, }; #[cfg(feature = "stargate")] use crate::calls::{ @@ -24,7 +27,7 @@ use crate::instance::Instance; use crate::serde::to_vec; use crate::{BackendApi, Querier, Storage}; -/// Mimicks the call signature of the smart contracts. +/// Mimics the call signature of the smart contracts. /// Thus it moves env and msg rather than take them as reference. /// This is inefficient here, but only used in test code. pub fn instantiate( @@ -40,11 +43,11 @@ where M: Serialize + JsonSchema, U: DeserializeOwned + CustomMsg, { - let serialized_msg = to_vec(&msg).expect("Testing error: Could not seralize request message"); + let serialized_msg = to_vec(&msg).expect("Testing error: Could not serialize request message"); call_instantiate(instance, &env, &info, &serialized_msg).expect("VM error") } -// execute mimicks the call signature of the smart contracts. +// execute mimics the call signature of the smart contracts. // thus it moves env and msg rather than take them as reference. // this is inefficient here, but only used in test code pub fn execute( @@ -60,11 +63,11 @@ where M: Serialize + JsonSchema, U: DeserializeOwned + CustomMsg, { - let serialized_msg = to_vec(&msg).expect("Testing error: Could not seralize request message"); + let serialized_msg = to_vec(&msg).expect("Testing error: Could not serialize request message"); call_execute(instance, &env, &info, &serialized_msg).expect("VM error") } -// migrate mimicks the call signature of the smart contracts. +// migrate mimics the call signature of the smart contracts. // thus it moves env and msg rather than take them as reference. // this is inefficient here, but only used in test code pub fn migrate( @@ -79,11 +82,31 @@ where M: Serialize + JsonSchema, U: DeserializeOwned + CustomMsg, { - let serialized_msg = to_vec(&msg).expect("Testing error: Could not seralize request message"); + let serialized_msg = to_vec(&msg).expect("Testing error: Could not serialize request message"); call_migrate(instance, &env, &serialized_msg).expect("VM error") } -// sudo mimicks the call signature of the smart contracts. +// migrate mimics the call signature of the smart contracts. +// thus it moves env and msg rather than take them as reference. +// this is inefficient here, but only used in test code +pub fn migrate_with_info( + instance: &mut Instance, + env: Env, + msg: M, + migrate_info: MigrateInfo, +) -> ContractResult> +where + A: BackendApi + 'static, + S: Storage + 'static, + Q: Querier + 'static, + M: Serialize + JsonSchema, + U: DeserializeOwned + CustomMsg, +{ + let serialized_msg = to_vec(&msg).expect("Testing error: Could not serialize request message"); + call_migrate_with_info(instance, &env, &serialized_msg, &migrate_info).expect("VM error") +} + +// sudo mimics the call signature of the smart contracts. // thus it moves env and msg rather than take them as reference. // this is inefficient here, but only used in test code pub fn sudo( @@ -98,11 +121,11 @@ where M: Serialize + JsonSchema, U: DeserializeOwned + CustomMsg, { - let serialized_msg = to_vec(&msg).expect("Testing error: Could not seralize request message"); + let serialized_msg = to_vec(&msg).expect("Testing error: Could not serialize request message"); call_sudo(instance, &env, &serialized_msg).expect("VM error") } -// reply mimicks the call signature of the smart contracts. +// reply mimics the call signature of the smart contracts. // thus it moves env and msg rather than take them as reference. // this is inefficient here, but only used in test code pub fn reply( @@ -119,7 +142,7 @@ where call_reply(instance, &env, &msg).expect("VM error") } -// query mimicks the call signature of the smart contracts. +// query mimics the call signature of the smart contracts. // thus it moves env and msg rather than take them as reference. // this is inefficient here, but only used in test code pub fn query( @@ -133,11 +156,11 @@ where Q: Querier + 'static, M: Serialize + JsonSchema, { - let serialized_msg = to_vec(&msg).expect("Testing error: Could not seralize request message"); + let serialized_msg = to_vec(&msg).expect("Testing error: Could not serialize request message"); call_query(instance, &env, &serialized_msg).expect("VM error") } -// ibc_channel_open mimicks the call signature of the smart contracts. +// ibc_channel_open mimics the call signature of the smart contracts. // thus it moves env and channel rather than take them as reference. // this is inefficient here, but only used in test code #[cfg(feature = "stargate")] @@ -154,7 +177,7 @@ where call_ibc_channel_open(instance, &env, &msg).expect("VM error") } -// ibc_channel_connect mimicks the call signature of the smart contracts. +// ibc_channel_connect mimics the call signature of the smart contracts. // thus it moves env and channel rather than take them as reference. // this is inefficient here, but only used in test code #[cfg(feature = "stargate")] @@ -172,7 +195,7 @@ where call_ibc_channel_connect(instance, &env, &msg).expect("VM error") } -// ibc_channel_close mimicks the call signature of the smart contracts. +// ibc_channel_close mimics the call signature of the smart contracts. // thus it moves env and channel rather than take them as reference. // this is inefficient here, but only used in test code #[cfg(feature = "stargate")] @@ -190,7 +213,7 @@ where call_ibc_channel_close(instance, &env, &msg).expect("VM error") } -// ibc_packet_receive mimicks the call signature of the smart contracts. +// ibc_packet_receive mimics the call signature of the smart contracts. // thus it moves env and packet rather than take them as reference. // this is inefficient here, but only used in test code #[cfg(feature = "stargate")] @@ -208,7 +231,7 @@ where call_ibc_packet_receive(instance, &env, &msg).expect("VM error") } -// ibc_packet_ack mimicks the call signature of the smart contracts. +// ibc_packet_ack mimics the call signature of the smart contracts. // thus it moves env and acknowledgement rather than take them as reference. // this is inefficient here, but only used in test code #[cfg(feature = "stargate")] @@ -226,7 +249,7 @@ where call_ibc_packet_ack(instance, &env, &msg).expect("VM error") } -// ibc_packet_timeout mimicks the call signature of the smart contracts. +// ibc_packet_timeout mimics the call signature of the smart contracts. // thus it moves env and packet rather than take them as reference. // this is inefficient here, but only used in test code #[cfg(feature = "stargate")] diff --git a/packages/vm/src/testing/mod.rs b/packages/vm/src/testing/mod.rs index 347258973b..495bcb37c2 100644 --- a/packages/vm/src/testing/mod.rs +++ b/packages/vm/src/testing/mod.rs @@ -6,7 +6,7 @@ mod mock; mod querier; mod storage; -pub use calls::{execute, instantiate, migrate, query, reply, sudo}; +pub use calls::{execute, instantiate, migrate, migrate_with_info, query, reply, sudo}; #[cfg(feature = "stargate")] pub use calls::{ ibc_channel_close, ibc_channel_connect, ibc_channel_open, ibc_packet_ack, ibc_packet_receive,