diff --git a/CHANGELOG.md b/CHANGELOG.md index 80b047cc1e..906e2b6732 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,8 @@ and this project adheres to different implementation) ([#692], [#711], [#714]) - cosmwasm-std: Added new `WasmMsg::Migrate` variant that allows one contract (eg. multisig) be the admin and migrate another contract ([#768]) +- cosmwasm-std: Added optional `system` entry point that can only be called by + native (blockchain) modules to expose admin functionality if desired. ([#793]) [#692]: https://github.com/CosmWasm/cosmwasm/issues/692 [#706]: https://github.com/CosmWasm/cosmwasm/pull/706 @@ -47,6 +49,7 @@ and this project adheres to [#714]: https://github.com/CosmWasm/cosmwasm/pull/714 [#716]: https://github.com/CosmWasm/cosmwasm/pull/716 [#768]: https://github.com/CosmWasm/cosmwasm/pull/768 +[#793]: https://github.com/CosmWasm/cosmwasm/pull/793 ### Changed diff --git a/contracts/hackatom/examples/schema.rs b/contracts/hackatom/examples/schema.rs index 86f59340cc..4287210a15 100644 --- a/contracts/hackatom/examples/schema.rs +++ b/contracts/hackatom/examples/schema.rs @@ -4,7 +4,9 @@ use std::fs::create_dir_all; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; use cosmwasm_std::BalanceResponse; -use hackatom::contract::{HandleMsg, InitMsg, MigrateMsg, QueryMsg, State, VerifierResponse}; +use hackatom::contract::{ + HandleMsg, InitMsg, MigrateMsg, QueryMsg, State, SystemMsg, VerifierResponse, +}; fn main() { let mut out_dir = current_dir().unwrap(); @@ -15,6 +17,7 @@ fn main() { export_schema(&schema_for!(InitMsg), &out_dir); export_schema(&schema_for!(HandleMsg), &out_dir); export_schema(&schema_for!(MigrateMsg), &out_dir); + export_schema(&schema_for!(SystemMsg), &out_dir); export_schema(&schema_for!(QueryMsg), &out_dir); export_schema(&schema_for!(State), &out_dir); export_schema(&schema_for!(VerifierResponse), &out_dir); diff --git a/contracts/hackatom/schema/system_msg.json b/contracts/hackatom/schema/system_msg.json new file mode 100644 index 0000000000..5fe06b713c --- /dev/null +++ b/contracts/hackatom/schema/system_msg.json @@ -0,0 +1,56 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SystemMsg", + "description": "SystemMsg is only expose for internal sdk modules to call. This is showing how we can expose \"admin\" functionality than can not be called by external users or contracts, but only trusted (native/Go) code in the blockchain", + "anyOf": [ + { + "type": "object", + "required": [ + "StealFunds" + ], + "properties": { + "StealFunds": { + "type": "object", + "required": [ + "amount", + "recipient" + ], + "properties": { + "amount": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "recipient": { + "$ref": "#/definitions/HumanAddr" + } + } + } + } + } + ], + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "HumanAddr": { + "type": "string" + }, + "Uint128": { + "type": "string" + } + } +} diff --git a/contracts/hackatom/src/contract.rs b/contracts/hackatom/src/contract.rs index b44eda5a8c..158cba0465 100644 --- a/contracts/hackatom/src/contract.rs +++ b/contracts/hackatom/src/contract.rs @@ -5,9 +5,9 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use cosmwasm_std::{ - from_slice, to_binary, to_vec, AllBalanceResponse, Api, BankMsg, Binary, CanonicalAddr, Deps, - DepsMut, Env, HumanAddr, MessageInfo, QueryRequest, QueryResponse, Response, StdError, - StdResult, WasmQuery, + entry_point, from_slice, to_binary, to_vec, AllBalanceResponse, Api, BankMsg, Binary, + CanonicalAddr, Coin, Deps, DepsMut, Env, HumanAddr, MessageInfo, QueryRequest, QueryResponse, + Response, StdError, StdResult, WasmQuery, }; use crate::errors::HackError; @@ -30,6 +30,17 @@ pub struct MigrateMsg { pub verifier: HumanAddr, } +/// SystemMsg is only expose for internal sdk modules to call. +/// This is showing how we can expose "admin" functionality than can not be called by +/// external users or contracts, but only trusted (native/Go) code in the blockchain +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub enum SystemMsg { + StealFunds { + recipient: HumanAddr, + amount: Vec, + }, +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct State { pub verifier: CanonicalAddr, @@ -122,6 +133,21 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result Result { + match msg { + SystemMsg::StealFunds { recipient, amount } => { + let msg = BankMsg::Send { + to_address: recipient, + amount, + }; + let mut response = Response::default(); + response.add_message(msg); + Ok(response) + } + } +} + pub fn handle( deps: DepsMut, env: Env, @@ -433,6 +459,34 @@ mod tests { assert_eq!(query_response.verifier, new_verifier); } + #[test] + fn system_can_steal_tokens() { + let mut deps = mock_dependencies(&[]); + + let verifier = HumanAddr::from("verifies"); + let beneficiary = HumanAddr::from("benefits"); + let creator = HumanAddr::from("creator"); + let msg = InitMsg { + verifier: verifier.clone(), + beneficiary, + }; + let info = mock_info(creator.as_str(), &[]); + let res = init(deps.as_mut(), mock_env(), info, msg).unwrap(); + assert_eq!(0, res.messages.len()); + + // system takes any tax it wants + let to_address = HumanAddr::from("community-pool"); + let amount = coins(700, "gold"); + let sys_msg = SystemMsg::StealFunds { + recipient: to_address.clone(), + amount: amount.clone(), + }; + let res = system(deps.as_mut(), mock_env(), sys_msg.clone()).unwrap(); + assert_eq!(1, res.messages.len()); + let msg = res.messages.get(0).expect("no message"); + assert_eq!(msg, &BankMsg::Send { to_address, amount }.into(),); + } + #[test] fn querier_callbacks_work() { let rich_addr = HumanAddr::from("foobar"); diff --git a/contracts/hackatom/tests/integration.rs b/contracts/hackatom/tests/integration.rs index b9c044fe07..88f2270f55 100644 --- a/contracts/hackatom/tests/integration.rs +++ b/contracts/hackatom/tests/integration.rs @@ -25,12 +25,12 @@ use cosmwasm_vm::{ call_handle, from_slice, testing::{ handle, init, migrate, mock_env, mock_info, mock_instance, mock_instance_with_balances, - query, test_io, MOCK_CONTRACT_ADDR, + query, system, test_io, MOCK_CONTRACT_ADDR, }, BackendApi, Storage, VmError, }; -use hackatom::contract::{HandleMsg, InitMsg, MigrateMsg, QueryMsg, State, CONFIG_KEY}; +use hackatom::contract::{HandleMsg, InitMsg, MigrateMsg, QueryMsg, State, SystemMsg, CONFIG_KEY}; static WASM: &[u8] = include_bytes!("../target/wasm32-unknown-unknown/release/hackatom.wasm"); @@ -145,6 +145,34 @@ fn migrate_verifier() { ); } +#[test] +fn system_can_steal_tokens() { + let mut deps = mock_instance(WASM, &[]); + + let verifier = HumanAddr::from("verifies"); + let beneficiary = HumanAddr::from("benefits"); + let creator = HumanAddr::from("creator"); + let msg = InitMsg { + verifier: verifier.clone(), + beneficiary, + }; + let info = mock_info(creator.as_str(), &[]); + let res: Response = init(&mut deps, mock_env(), info, msg).unwrap(); + assert_eq!(0, res.messages.len()); + + // system takes any tax it wants + let to_address = HumanAddr::from("community-pool"); + let amount = coins(700, "gold"); + let sys_msg = SystemMsg::StealFunds { + recipient: to_address.clone(), + amount: amount.clone(), + }; + let res: Response = system(&mut deps, mock_env(), sys_msg.clone()).unwrap(); + assert_eq!(1, res.messages.len()); + let msg = res.messages.get(0).expect("no message"); + assert_eq!(msg, &BankMsg::Send { to_address, amount }.into(),); +} + #[test] fn querier_callbacks_work() { let rich_addr = HumanAddr::from("foobar"); diff --git a/packages/std/src/exports.rs b/packages/std/src/exports.rs index eba58def5b..374e92d882 100644 --- a/packages/std/src/exports.rs +++ b/packages/std/src/exports.rs @@ -136,6 +136,26 @@ where release_buffer(v) as u32 } +/// do_system should be wrapped in an external "C" export, containing a contract-specific function as arg +/// +/// - `M`: message type for request +/// - `C`: custom response message type (see CosmosMsg) +/// - `E`: error type for responses +pub fn do_system( + system_fn: &dyn Fn(DepsMut, Env, M) -> Result, E>, + env_ptr: u32, + msg_ptr: u32, +) -> u32 +where + M: DeserializeOwned + JsonSchema, + C: Serialize + Clone + fmt::Debug + PartialEq + JsonSchema, + E: ToString, +{ + let res = _do_system(system_fn, env_ptr as *mut Region, msg_ptr as *mut Region); + let v = to_vec(&res).unwrap(); + release_buffer(v) as u32 +} + /// do_query should be wrapped in an external "C" export, containing a contract-specific function as arg /// /// - `M`: message type for request @@ -220,6 +240,26 @@ where migrate_fn(deps.as_mut(), env, msg).into() } +fn _do_system( + system_fn: &dyn Fn(DepsMut, Env, M) -> Result, E>, + env_ptr: *mut Region, + msg_ptr: *mut Region, +) -> ContractResult> +where + M: DeserializeOwned + JsonSchema, + C: Serialize + Clone + fmt::Debug + PartialEq + JsonSchema, + E: ToString, +{ + let env: Vec = unsafe { consume_region(env_ptr) }; + let msg: Vec = unsafe { consume_region(msg_ptr) }; + + let env: Env = try_into_contract_result!(from_slice(&env)); + let msg: M = try_into_contract_result!(from_slice(&msg)); + + let mut deps = make_dependencies(); + system_fn(deps.as_mut(), env, msg).into() +} + fn _do_query( query_fn: &dyn Fn(Deps, Env, M) -> Result, env_ptr: *mut Region, diff --git a/packages/std/src/lib.rs b/packages/std/src/lib.rs index 8a8f04840e..c5bdef9545 100644 --- a/packages/std/src/lib.rs +++ b/packages/std/src/lib.rs @@ -62,7 +62,7 @@ mod imports; mod memory; // Used by exports and imports only. This assumes pointers are 32 bit long, which makes it untestable on dev machines. #[cfg(target_arch = "wasm32")] -pub use crate::exports::{do_handle, do_init, do_migrate, do_query}; +pub use crate::exports::{do_handle, do_init, do_migrate, do_query, do_system}; #[cfg(target_arch = "wasm32")] pub use crate::imports::{ExternalApi, ExternalQuerier, ExternalStorage}; diff --git a/packages/vm/src/calls.rs b/packages/vm/src/calls.rs index f569c9f196..be06cab170 100644 --- a/packages/vm/src/calls.rs +++ b/packages/vm/src/calls.rs @@ -14,6 +14,7 @@ use crate::serde::{from_slice, to_vec}; const MAX_LENGTH_INIT: usize = 100_000; const MAX_LENGTH_HANDLE: usize = 100_000; const MAX_LENGTH_MIGRATE: usize = 100_000; +const MAX_LENGTH_SYSTEM: usize = 100_000; const MAX_LENGTH_QUERY: usize = 100_000; pub fn call_init( @@ -71,6 +72,23 @@ where Ok(result) } +pub fn call_system( + instance: &mut Instance, + env: &Env, + msg: &[u8], +) -> VmResult>> +where + A: BackendApi + 'static, + S: Storage + 'static, + Q: Querier + 'static, + U: DeserializeOwned + Clone + fmt::Debug + JsonSchema + PartialEq, +{ + let env = to_vec(env)?; + let data = call_system_raw(instance, &env, msg)?; + let result: ContractResult> = from_slice(&data)?; + Ok(result) +} + pub fn call_query( instance: &mut Instance, env: &Env, @@ -144,6 +162,22 @@ where call_raw(instance, "migrate", &[env, msg], MAX_LENGTH_MIGRATE) } +/// Calls Wasm export "system" and returns raw data from the contract. +/// The result is length limited to prevent abuse but otherwise unchecked. +pub fn call_system_raw( + instance: &mut Instance, + env: &[u8], + msg: &[u8], +) -> VmResult> +where + A: BackendApi + 'static, + S: Storage + 'static, + Q: Querier + 'static, +{ + instance.set_storage_readonly(false); + call_raw(instance, "system", &[env, msg], MAX_LENGTH_SYSTEM) +} + /// Calls Wasm export "query" and returns raw data from the contract. /// The result is length limited to prevent abuse but otherwise unchecked. pub fn call_query_raw( diff --git a/packages/vm/src/instance.rs b/packages/vm/src/instance.rs index 13e3e526e5..d2bcf39237 100644 --- a/packages/vm/src/instance.rs +++ b/packages/vm/src/instance.rs @@ -811,7 +811,7 @@ mod singlepass_tests { .unwrap(); let handle_used = gas_before_handle - instance.get_gas_left(); - assert_eq!(handle_used, 165757); + assert_eq!(handle_used, 165794); } #[test] diff --git a/packages/vm/src/lib.rs b/packages/vm/src/lib.rs index 6b862bea85..d2a51511ee 100644 --- a/packages/vm/src/lib.rs +++ b/packages/vm/src/lib.rs @@ -29,7 +29,7 @@ pub use crate::backend::{ pub use crate::cache::{AnalysisReport, Cache, CacheOptions, Stats}; pub use crate::calls::{ call_handle, call_handle_raw, call_init, call_init_raw, call_migrate, call_migrate_raw, - call_query, call_query_raw, + call_query, call_query_raw, call_system, call_system_raw, }; pub use crate::checksum::Checksum; diff --git a/packages/vm/src/static_analysis.rs b/packages/vm/src/static_analysis.rs index a8796e462a..a2fad8da34 100644 --- a/packages/vm/src/static_analysis.rs +++ b/packages/vm/src/static_analysis.rs @@ -73,7 +73,7 @@ mod tests { false } }); - assert_eq!(exported_functions.count(), 7); // 6 required export plus "migrate" + assert_eq!(exported_functions.count(), 8); // 6 required export plus "migrate" and "system" let exported_memories = module .export_section() diff --git a/packages/vm/src/testing/calls.rs b/packages/vm/src/testing/calls.rs index ecc1dd0731..1f157642ad 100644 --- a/packages/vm/src/testing/calls.rs +++ b/packages/vm/src/testing/calls.rs @@ -7,7 +7,7 @@ use std::fmt; use cosmwasm_std::{ContractResult, Env, MessageInfo, QueryResponse, Response}; -use crate::calls::{call_handle, call_init, call_migrate, call_query}; +use crate::calls::{call_handle, call_init, call_migrate, call_query, call_system}; use crate::instance::Instance; use crate::serde::to_vec; use crate::{BackendApi, Querier, Storage}; @@ -71,6 +71,25 @@ where call_migrate(instance, &env, &serialized_msg).expect("VM error") } +// system mimicks 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 system( + instance: &mut Instance, + env: Env, + msg: M, +) -> ContractResult> +where + A: BackendApi + 'static, + S: Storage + 'static, + Q: Querier + 'static, + M: Serialize + JsonSchema, + U: DeserializeOwned + Clone + PartialEq + JsonSchema + fmt::Debug, +{ + let serialized_msg = to_vec(&msg).expect("Testing error: Could not seralize request message"); + call_system(instance, &env, &serialized_msg).expect("VM error") +} + // query mimicks 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 diff --git a/packages/vm/src/testing/mod.rs b/packages/vm/src/testing/mod.rs index a31bc76473..06edcc7bfd 100644 --- a/packages/vm/src/testing/mod.rs +++ b/packages/vm/src/testing/mod.rs @@ -7,7 +7,7 @@ mod mock; mod querier; mod storage; -pub use calls::{handle, init, migrate, query}; +pub use calls::{handle, init, migrate, query, system}; #[cfg(feature = "stargate")] pub use ibc_calls::{ ibc_channel_close, ibc_channel_connect, ibc_channel_open, ibc_packet_ack, ibc_packet_receive, diff --git a/packages/vm/testdata/hackatom_0.14.wasm b/packages/vm/testdata/hackatom_0.14.wasm index bf32508c9a..3ca30fcd9b 100644 Binary files a/packages/vm/testdata/hackatom_0.14.wasm and b/packages/vm/testdata/hackatom_0.14.wasm differ