From aed786033a7a6ae55ef5f59d044c53b33f018568 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Tue, 9 Jan 2024 14:04:52 -0500 Subject: [PATCH 01/15] Add pausable ism with tests --- contracts/isms/pausable/Cargo.toml | 43 +++++++ contracts/isms/pausable/src/lib.rs | 164 +++++++++++++++++++++++++ packages/interface/src/ism/mod.rs | 1 + packages/interface/src/ism/pausable.rs | 26 ++++ 4 files changed, 234 insertions(+) create mode 100644 contracts/isms/pausable/Cargo.toml create mode 100644 contracts/isms/pausable/src/lib.rs create mode 100644 packages/interface/src/ism/pausable.rs diff --git a/contracts/isms/pausable/Cargo.toml b/contracts/isms/pausable/Cargo.toml new file mode 100644 index 00000000..41cef4a1 --- /dev/null +++ b/contracts/isms/pausable/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "hpl-ism-pausable" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +keywords.workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-std.workspace = true +cosmwasm-storage.workspace = true +cosmwasm-schema.workspace = true + +cw-storage-plus.workspace = true +cw2.workspace = true +cw-utils.workspace = true + +schemars.workspace = true +serde-json-wasm.workspace = true + +thiserror.workspace = true + +hpl-ownable.workspace = true +hpl-pausable.workspace = true +hpl-interface.workspace = true + +[dev-dependencies] +rstest.workspace = true +ibcx-test-utils.workspace = true + +anyhow.workspace = true diff --git a/contracts/isms/pausable/src/lib.rs b/contracts/isms/pausable/src/lib.rs new file mode 100644 index 00000000..51c44146 --- /dev/null +++ b/contracts/isms/pausable/src/lib.rs @@ -0,0 +1,164 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + ensure, to_json_binary, Deps, DepsMut, Env, Event, MessageInfo, QueryResponse, Response, + StdError, +}; +use hpl_interface::ism::{ + pausable::{ExecuteMsg, InstantiateMsg, QueryMsg}, + IsmQueryMsg, IsmType, ModuleTypeResponse, VerifyResponse, +}; + +#[derive(thiserror::Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + PaymentError(#[from] cw_utils::PaymentError), + + #[error("unauthorized")] + Unauthorized {}, + + #[error("hook paused")] + Paused {}, +} + +// version info for migration info +pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +fn new_event(name: &str) -> Event { + Event::new(format!("hpl_hook_pausable::{}", name)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + let owner = deps.api.addr_validate(&msg.owner)?; + + hpl_ownable::initialize(deps.storage, &owner)?; + hpl_pausable::initialize(deps.storage, &msg.paused)?; + + Ok(Response::new().add_event( + new_event("initialize") + .add_attribute("sender", info.sender) + .add_attribute("owner", owner), + )) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Ownable(msg) => Ok(hpl_ownable::handle(deps, env, info, msg)?), + ExecuteMsg::Pausable(msg) => Ok(hpl_pausable::handle(deps, env, info, msg)?), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { + use IsmQueryMsg::*; + + match msg { + QueryMsg::Pausable(msg) => Ok(hpl_pausable::handle_query(deps, env, msg)?), + QueryMsg::Ownable(msg) => Ok(hpl_ownable::handle_query(deps, env, msg)?), + QueryMsg::Ism(msg) => match msg { + ModuleType {} => Ok(to_json_binary(&ModuleTypeResponse { typ: IsmType::Null })?), + Verify { + metadata: _, + message: _, + } => { + ensure!( + !hpl_pausable::get_pause_info(deps.storage)?, + ContractError::Paused {} + ); + Ok(to_json_binary(&VerifyResponse { verified: true })?) + } + _ => unimplemented!(), + }, + } +} + +#[cfg(test)] +mod test { + use cosmwasm_schema::serde::{de::DeserializeOwned, Serialize}; + use cosmwasm_std::{ + from_json, + testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage}, + to_json_binary, Addr, OwnedDeps, + }; + use hpl_ownable::get_owner; + use hpl_pausable::get_pause_info; + use ibcx_test_utils::{addr, hex}; + use rstest::{fixture, rstest}; + + use super::*; + + type TestDeps = OwnedDeps; + + fn query(deps: Deps, msg: S) -> T { + let req: QueryMsg = from_json(to_json_binary(&msg).unwrap()).unwrap(); + let res = crate::query(deps, mock_env(), req).unwrap(); + from_json(res).unwrap() + } + + #[fixture] + fn deps( + #[default(addr("deployer"))] sender: Addr, + #[default(addr("owner"))] owner: Addr, + #[default(false)] paused: bool, + ) -> TestDeps { + let mut deps = mock_dependencies(); + + instantiate( + deps.as_mut(), + mock_env(), + mock_info(sender.as_str(), &[]), + InstantiateMsg { + owner: owner.to_string(), + paused, + }, + ) + .unwrap(); + + deps + } + + #[rstest] + fn test_init(deps: TestDeps) { + assert!(!get_pause_info(deps.as_ref().storage).unwrap()); + assert_eq!("owner", get_owner(deps.as_ref().storage).unwrap().as_str()); + } + + #[rstest] + #[case(false)] + #[should_panic(expected = "hook paused")] + #[case(true)] + fn test_query(mut deps: TestDeps, #[case] paused: bool) { + if paused { + hpl_pausable::pause(deps.as_mut().storage, &addr("owner")).unwrap(); + } + + let raw_message = hex("0000000000000068220000000000000000000000000d1255b09d94659bb0888e0aa9fca60245ce402a0000682155208cd518cffaac1b5d8df216a9bd050c9a03f0d4f3ba88e5268ac4cd12ee2d68656c6c6f"); + let raw_metadata = raw_message.clone(); + + query::<_, VerifyResponse>( + deps.as_ref(), + QueryMsg::Ism(IsmQueryMsg::Verify { + metadata: raw_metadata, + message: raw_message, + }), + ); + } +} diff --git a/packages/interface/src/ism/mod.rs b/packages/interface/src/ism/mod.rs index 13cccbe4..f3947053 100644 --- a/packages/interface/src/ism/mod.rs +++ b/packages/interface/src/ism/mod.rs @@ -1,6 +1,7 @@ pub mod aggregate; pub mod multisig; pub mod routing; +pub mod pausable; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, CustomQuery, HexBinary, QuerierWrapper, StdResult}; diff --git a/packages/interface/src/ism/pausable.rs b/packages/interface/src/ism/pausable.rs new file mode 100644 index 00000000..1f48934f --- /dev/null +++ b/packages/interface/src/ism/pausable.rs @@ -0,0 +1,26 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; + +use crate::{ownable::{OwnableMsg, OwnableQueryMsg}, pausable::{PausableMsg, PausableQueryMsg}}; + +use super::IsmQueryMsg; + +#[cw_serde] +pub struct InstantiateMsg { + pub owner: String, + pub paused: bool +} + +#[cw_serde] +pub enum ExecuteMsg { + Ownable(OwnableMsg), + Pausable(PausableMsg) +} + +#[cw_serde] +#[derive(QueryResponses)] +#[query_responses(nested)] +pub enum QueryMsg { + Ownable(OwnableQueryMsg), + Ism(IsmQueryMsg), + Pausable(PausableQueryMsg) +} From ce5c82b6752c354b59ed4c115135cdac489cd8b1 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Tue, 9 Jan 2024 14:08:49 -0500 Subject: [PATCH 02/15] Fix paused query error case --- contracts/isms/pausable/src/lib.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/contracts/isms/pausable/src/lib.rs b/contracts/isms/pausable/src/lib.rs index 51c44146..d89ebe22 100644 --- a/contracts/isms/pausable/src/lib.rs +++ b/contracts/isms/pausable/src/lib.rs @@ -107,10 +107,9 @@ mod test { type TestDeps = OwnedDeps; - fn query(deps: Deps, msg: S) -> T { + fn query(deps: Deps, msg: crate::QueryMsg) -> Result { let req: QueryMsg = from_json(to_json_binary(&msg).unwrap()).unwrap(); - let res = crate::query(deps, mock_env(), req).unwrap(); - from_json(res).unwrap() + crate::query(deps, mock_env(), req) } #[fixture] @@ -153,12 +152,14 @@ mod test { let raw_message = hex("0000000000000068220000000000000000000000000d1255b09d94659bb0888e0aa9fca60245ce402a0000682155208cd518cffaac1b5d8df216a9bd050c9a03f0d4f3ba88e5268ac4cd12ee2d68656c6c6f"); let raw_metadata = raw_message.clone(); - query::<_, VerifyResponse>( + query( deps.as_ref(), QueryMsg::Ism(IsmQueryMsg::Verify { metadata: raw_metadata, message: raw_message, }), - ); + ) + .map_err(|e| e.to_string()) + .unwrap(); } } From f623cbc5c65119e4e667b6697f280a6d953462ce Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Tue, 9 Jan 2024 14:19:55 -0500 Subject: [PATCH 03/15] Run CI against all PRs --- .github/workflows/test.yaml | 2 -- contracts/isms/pausable/src/lib.rs | 1 - 2 files changed, 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 7959e3cd..ae8bdc8f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -2,8 +2,6 @@ name: test on: pull_request: - branches: - - "main" push: branches: - "main" diff --git a/contracts/isms/pausable/src/lib.rs b/contracts/isms/pausable/src/lib.rs index d89ebe22..16b82b85 100644 --- a/contracts/isms/pausable/src/lib.rs +++ b/contracts/isms/pausable/src/lib.rs @@ -92,7 +92,6 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result Date: Tue, 9 Jan 2024 14:22:53 -0500 Subject: [PATCH 04/15] Add pausable ISM to README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 79814e61..fe00de58 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,8 @@ grcov . -s . --binary-path ./target/debug/ -t lcov --branch --ignore-not-existin - [aggregate ism](./contracts/isms/aggregate) + - [pausable](./contracts/isms/pausable) + - For testing: [mock ism](./contracts/mocks/mock-ism) 5. Set deployed hooks and isms to Mailbox From d9d89378b65bc4709cea9a4c64cbdff03bfe4f28 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Tue, 9 Jan 2024 16:46:04 -0500 Subject: [PATCH 05/15] Fix scripts --- scripts/action/ism.ts | 14 +++++++------- scripts/action/mailbox.ts | 18 +++++++++--------- scripts/src/contracts/hpl_ism_pausable.ts | 7 +++++++ scripts/src/contracts/index.ts | 5 +++-- scripts/src/migrations/InitializeStandalone.ts | 9 +++++---- scripts/src/types.ts | 10 +++++----- 6 files changed, 36 insertions(+), 27 deletions(-) create mode 100644 scripts/src/contracts/hpl_ism_pausable.ts diff --git a/scripts/action/ism.ts b/scripts/action/ism.ts index 897a8d3f..f842d776 100644 --- a/scripts/action/ism.ts +++ b/scripts/action/ism.ts @@ -1,17 +1,17 @@ -import { Command } from "commander"; import { ExecuteResult } from "@cosmjs/cosmwasm-stargate"; +import { Command } from "commander"; import { version } from "../package.json"; import { config, getSigningClient } from "../src/config"; -import { loadContext } from "../src/load_context"; -import { ContractFetcher } from "./fetch"; import { - HplMailbox, - HplIgp, - HplIgpGasOracle, HplHookMerkle, + HplIgp, + HplIgpOracle, HplIsmAggregate, + HplMailbox, } from "../src/contracts"; +import { loadContext } from "../src/load_context"; +import { ContractFetcher } from "./fetch"; const program = new Command(); @@ -51,7 +51,7 @@ function makeHandler( const fetcher = new ContractFetcher(ctx, client); const mailbox = fetcher.get(HplMailbox, "hpl_mailbox"); const igp = fetcher.get(HplIgp, "hpl_igp"); - const igp_oracle = fetcher.get(HplIgpGasOracle, "hpl_igp_oracle"); + const igp_oracle = fetcher.get(HplIgpOracle, "hpl_igp_oracle"); const hook_merkle = fetcher.get(HplHookMerkle, "hpl_hook_merkle"); const hook_aggregate = fetcher.get(HplIsmAggregate, "hpl_hook_aggregate"); diff --git a/scripts/action/mailbox.ts b/scripts/action/mailbox.ts index 2509708d..bd40ea1a 100644 --- a/scripts/action/mailbox.ts +++ b/scripts/action/mailbox.ts @@ -1,18 +1,18 @@ -import { Command } from "commander"; import { ExecuteResult } from "@cosmjs/cosmwasm-stargate"; +import { Command } from "commander"; import { version } from "../package.json"; import { config, getSigningClient } from "../src/config"; +import { + HplHookMerkle, + HplIgp, + HplIgpOracle, + HplIsmAggregate, + HplMailbox, +} from "../src/contracts"; import { addPad } from "../src/conv"; import { loadContext } from "../src/load_context"; import { ContractFetcher } from "./fetch"; -import { - HplMailbox, - HplIgp, - HplIgpGasOracle, - HplHookMerkle, - HplIsmAggregate, -} from "../src/contracts"; const program = new Command(); @@ -54,7 +54,7 @@ function makeHandler( const fetcher = new ContractFetcher(ctx, client); const mailbox = fetcher.get(HplMailbox, "hpl_mailbox"); const igp = fetcher.get(HplIgp, "hpl_igp"); - const igp_oracle = fetcher.get(HplIgpGasOracle, "hpl_igp_oracle"); + const igp_oracle = fetcher.get(HplIgpOracle, "hpl_igp_oracle"); const hook_merkle = fetcher.get(HplHookMerkle, "hpl_hook_merkle"); const hook_aggregate = fetcher.get(HplIsmAggregate, "hpl_hook_aggregate"); diff --git a/scripts/src/contracts/hpl_ism_pausable.ts b/scripts/src/contracts/hpl_ism_pausable.ts new file mode 100644 index 00000000..7fce739a --- /dev/null +++ b/scripts/src/contracts/hpl_ism_pausable.ts @@ -0,0 +1,7 @@ +import { injectable } from "inversify"; +import { BaseContract } from "../types"; + +@injectable() +export class HplIsmPausable extends BaseContract { + contractName: string = "hpl_ism_pausable"; +} diff --git a/scripts/src/contracts/index.ts b/scripts/src/contracts/index.ts index 8016e991..417f7be2 100644 --- a/scripts/src/contracts/index.ts +++ b/scripts/src/contracts/index.ts @@ -8,6 +8,7 @@ export * from "./hpl_igp"; export * from "./hpl_igp_oracle"; export * from "./hpl_ism_aggregate"; export * from "./hpl_ism_multisig"; +export * from "./hpl_ism_pausable"; export * from "./hpl_ism_routing"; export * from "./hpl_mailbox"; export * from "./hpl_test_mock_hook"; @@ -16,10 +17,10 @@ export * from "./hpl_validator_announce"; export * from "./hpl_warp_cw20"; export * from "./hpl_warp_native"; -import { readdirSync } from "fs"; -import { Context, Contract, ContractConstructor } from "../types"; import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate"; +import { readdirSync } from "fs"; import { Container } from "inversify"; +import { Context, Contract, ContractConstructor } from "../types"; const contractNames: string[] = readdirSync(__dirname) .filter((f) => f !== "index.ts") diff --git a/scripts/src/migrations/InitializeStandalone.ts b/scripts/src/migrations/InitializeStandalone.ts index 920cc5b4..1bede003 100644 --- a/scripts/src/migrations/InitializeStandalone.ts +++ b/scripts/src/migrations/InitializeStandalone.ts @@ -1,12 +1,13 @@ import { injectable } from "inversify"; -import { Context, Migration } from "../types"; import { - HplMailbox, HplHookMerkle, - HplIgpGasOracle, + HplIgp, + HplIgpOracle, HplIsmMultisig, + HplMailbox, HplTestMockHook, } from "../contracts"; +import { Context, Migration } from "../types"; @injectable() export default class InitializeStandalone implements Migration { @@ -18,7 +19,7 @@ export default class InitializeStandalone implements Migration { private mailbox: HplMailbox, private hook_merkle: HplHookMerkle, private igp: HplIgp, - private igp_oracle: HplIgpGasOracle, + private igp_oracle: HplIgpOracle, private ism_multisig: HplIsmMultisig, private test_mock_hook: HplTestMockHook ) {} diff --git a/scripts/src/types.ts b/scripts/src/types.ts index b28f9edc..95c63af6 100644 --- a/scripts/src/types.ts +++ b/scripts/src/types.ts @@ -1,10 +1,10 @@ import { - ExecuteResult, - SigningCosmWasmClient, + ExecuteResult, + SigningCosmWasmClient, } from "@cosmjs/cosmwasm-stargate"; -import { getWasmPath } from "./load_wasm"; -import fs from "fs"; import { fromBech32 } from "@cosmjs/encoding"; +import fs from "fs"; +import { getWasmPath } from "./load_wasm"; export interface ContractContext { codeId: number | undefined; @@ -165,7 +165,7 @@ export interface HplIgpCoreInstantiateMsg { beneficiary: string; } -export interface HplIgpGasOracleInstantiateMsg {} +export interface HplIgpOracleInstantiateMsg {} export interface HplIsmMultisigInstantiateMsg { owner: string; From 496e66b91d8e85754cdacabccd16fe20214dd899 Mon Sep 17 00:00:00 2001 From: nambrot Date: Tue, 9 Jan 2024 17:32:25 -0500 Subject: [PATCH 06/15] Allow threshold == set size and add tests --- contracts/isms/multisig/src/contract.rs | 76 +++++- contracts/isms/multisig/src/execute.rs | 319 ------------------------ 2 files changed, 75 insertions(+), 320 deletions(-) delete mode 100644 contracts/isms/multisig/src/execute.rs diff --git a/contracts/isms/multisig/src/contract.rs b/contracts/isms/multisig/src/contract.rs index ce4075a1..6741189d 100644 --- a/contracts/isms/multisig/src/contract.rs +++ b/contracts/isms/multisig/src/contract.rs @@ -64,7 +64,7 @@ pub fn execute( ContractError::invalid_addr("length should be 20") ); ensure!( - validators.len() > threshold as usize && threshold > 0, + validators.len() >= threshold as usize && threshold > 0, ContractError::invalid_args(&format!( "threshold not in range. 0 < <= {}", validators.len(), @@ -137,3 +137,77 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result Result { Ok(Response::new()) } + +#[cfg(test)] +mod test { + use cosmwasm_std::{ + testing::mock_dependencies, HexBinary, + }; + use hpl_interface::{ + build_test_executor, build_test_querier, + ism::multisig::ExecuteMsg, + }; + use ibcx_test_utils::{addr, hex}; + use rstest::rstest; + + use crate::state::VALIDATORS; + + build_test_executor!(crate::contract::execute); + build_test_querier!(crate::contract::query); + + #[rstest] + #[case("owner", vec![hex(&"deadbeef".repeat(5))])] + #[should_panic(expected = "unauthorized")] + #[case("someone", vec![hex(&"deadbeef".repeat(5))])] + fn test_enroll(#[case] sender: &str, #[case] validators: Vec) { + let mut deps = mock_dependencies(); + + hpl_ownable::initialize(deps.as_mut().storage, &addr("owner")).unwrap(); + + test_execute( + deps.as_mut(), + &addr(sender), + ExecuteMsg::SetValidators { + domain: 1, + threshold: 1, + validators: validators.clone(), + }, + vec![], + ); + + assert_eq!( + VALIDATORS.load(deps.as_ref().storage, 1).unwrap(), + validators + ); + } + + #[rstest] + #[case("owner")] + #[should_panic(expected = "unauthorized")] + #[case("someone")] + fn test_unenroll(#[case] sender: &str) { + let mut deps = mock_dependencies(); + + hpl_ownable::initialize(deps.as_mut().storage, &addr("owner")).unwrap(); + + test_execute( + deps.as_mut(), + &addr("owner"), + ExecuteMsg::SetValidators { + domain: 1, + threshold: 1, + validators: vec![hex(&"deadbeef".repeat(5))], + }, + vec![], + ); + + test_execute( + deps.as_mut(), + &addr(sender), + ExecuteMsg::UnsetDomain { domain: 1 } , + vec![], + ); + + assert!(!VALIDATORS.has(deps.as_ref().storage, 1)); + } +} diff --git a/contracts/isms/multisig/src/execute.rs b/contracts/isms/multisig/src/execute.rs deleted file mode 100644 index d4661fbb..00000000 --- a/contracts/isms/multisig/src/execute.rs +++ /dev/null @@ -1,319 +0,0 @@ -use cosmwasm_std::{ensure_eq, DepsMut, Event, HexBinary, MessageInfo, Response, StdResult}; -use hpl_interface::ism::multisig::{ThresholdSet, ValidatorSet as MsgValidatorSet}; -use hpl_ownable::get_owner; - -use crate::{ - event::{emit_enroll_validator, emit_set_threshold, emit_unenroll_validator}, - state::{THRESHOLD, VALIDATORS}, - ContractError, -}; - -pub fn set_threshold( - deps: DepsMut, - info: MessageInfo, - threshold: ThresholdSet, -) -> Result { - ensure_eq!( - get_owner(deps.storage)?, - info.sender, - ContractError::Unauthorized - ); - THRESHOLD.save(deps.storage, threshold.domain, &threshold.threshold)?; - - Ok(Response::new().add_event(emit_set_threshold(threshold.domain, threshold.threshold))) -} - -pub fn set_thresholds( - deps: DepsMut, - info: MessageInfo, - thresholds: Vec, -) -> Result { - ensure_eq!( - get_owner(deps.storage)?, - info.sender, - ContractError::Unauthorized - ); - - let events: Vec = thresholds - .into_iter() - .map(|v| { - THRESHOLD.save(deps.storage, v.domain, &v.threshold)?; - Ok(emit_set_threshold(v.domain, v.threshold)) - }) - .collect::>()?; - - Ok(Response::new().add_events(events)) -} - -pub fn enroll_validator( - deps: DepsMut, - info: MessageInfo, - msg: MsgValidatorSet, -) -> Result { - ensure_eq!( - info.sender, - get_owner(deps.storage)?, - ContractError::Unauthorized {} - ); - - ensure_eq!( - msg.validator.len(), - 20, - ContractError::invalid_addr("length should be 20") - ); - - let validator_state = VALIDATORS.may_load(deps.storage, msg.domain)?; - - if let Some(mut validators) = validator_state { - if validators.contains(&msg.validator) { - return Err(ContractError::ValidatorDuplicate {}); - } - - validators.push(msg.validator.clone()); - validators.sort(); - - VALIDATORS.save(deps.storage, msg.domain, &validators)?; - } else { - VALIDATORS.save(deps.storage, msg.domain, &vec![msg.validator.clone()])?; - } - - Ok(Response::new().add_event(emit_enroll_validator(msg.domain, msg.validator.to_hex()))) -} - -pub fn enroll_validators( - deps: DepsMut, - info: MessageInfo, - validators: Vec, -) -> Result { - ensure_eq!( - info.sender, - get_owner(deps.storage)?, - ContractError::Unauthorized {} - ); - - let mut events: Vec = Vec::new(); - - for msg in validators.into_iter() { - ensure_eq!( - msg.validator.len(), - 20, - ContractError::invalid_addr("length should be 20") - ); - - let validators_state = VALIDATORS.may_load(deps.storage, msg.domain)?; - - if let Some(mut validators) = validators_state { - if validators.contains(&msg.validator) { - return Err(ContractError::ValidatorDuplicate {}); - } - - validators.push(msg.validator.clone()); - validators.sort(); - - VALIDATORS.save(deps.storage, msg.domain, &validators)?; - events.push(emit_enroll_validator(msg.domain, msg.validator.to_hex())); - } else { - VALIDATORS.save(deps.storage, msg.domain, &vec![msg.validator.clone()])?; - events.push(emit_enroll_validator(msg.domain, msg.validator.to_hex())); - } - } - - Ok(Response::new().add_events(events)) -} - -pub fn unenroll_validator( - deps: DepsMut, - info: MessageInfo, - domain: u32, - validator: HexBinary, -) -> Result { - ensure_eq!( - info.sender, - get_owner(deps.storage)?, - ContractError::Unauthorized {} - ); - - let validators = VALIDATORS - .load(deps.storage, domain) - .map_err(|_| ContractError::ValidatorNotExist {})?; - - if !validators.contains(&validator) { - return Err(ContractError::ValidatorNotExist {}); - } - - let mut validator_list: Vec = - validators.into_iter().filter(|v| v != &validator).collect(); - - validator_list.sort(); - - VALIDATORS.save(deps.storage, domain, &validator_list)?; - - Ok(Response::new().add_event(emit_unenroll_validator(domain, validator.to_hex()))) -} - -#[cfg(test)] -mod test { - use cosmwasm_std::{ - testing::{mock_dependencies, mock_info}, - Addr, HexBinary, Storage, - }; - use hpl_interface::{ - build_test_executor, build_test_querier, - ism::multisig::{ExecuteMsg, ValidatorSet}, - }; - use ibcx_test_utils::{addr, hex}; - use rstest::rstest; - - use crate::state::VALIDATORS; - - build_test_executor!(crate::contract::execute); - build_test_querier!(crate::contract::query); - - use super::*; - const ADDR1_VAULE: &str = "addr1"; - const ADDR2_VAULE: &str = "addr2"; - - fn mock_owner(storage: &mut dyn Storage, owner: Addr) { - hpl_ownable::initialize(storage, &owner).unwrap(); - } - - #[test] - fn test_set_threshold() { - let mut deps = mock_dependencies(); - let owner = Addr::unchecked(ADDR1_VAULE); - mock_owner(deps.as_mut().storage, owner.clone()); - - let threshold = ThresholdSet { - domain: 1u32, - threshold: 8u8, - }; - - // set_threshold failure test - let info = mock_info(ADDR2_VAULE, &[]); - let fail_result = set_threshold(deps.as_mut(), info, threshold.clone()).unwrap_err(); - - assert!(matches!(fail_result, ContractError::Unauthorized {})); - - // set_threshold success test - let info = mock_info(owner.as_str(), &[]); - let result = set_threshold(deps.as_mut(), info, threshold.clone()).unwrap(); - - assert_eq!( - result.events, - vec![emit_set_threshold(threshold.domain, threshold.threshold)] - ); - - // check it actually saved - let saved_threshold = THRESHOLD.load(&deps.storage, threshold.domain).unwrap(); - assert_eq!(saved_threshold, threshold.threshold); - } - - #[test] - fn test_set_thresholds() { - let mut deps = mock_dependencies(); - let owner = Addr::unchecked(ADDR1_VAULE); - mock_owner(deps.as_mut().storage, owner.clone()); - - let thresholds: Vec = vec![ - ThresholdSet { - domain: 1u32, - threshold: 8u8, - }, - ThresholdSet { - domain: 2u32, - threshold: 7u8, - }, - ThresholdSet { - domain: 3u32, - threshold: 6u8, - }, - ]; - - // set_threshold failure test - let info = mock_info(ADDR2_VAULE, &[]); - let fail_result = set_thresholds(deps.as_mut(), info, thresholds.clone()).unwrap_err(); - - assert!(matches!(fail_result, ContractError::Unauthorized {})); - - // set_threshold success test - let info = mock_info(owner.as_str(), &[]); - let result = set_thresholds(deps.as_mut(), info, thresholds.clone()).unwrap(); - - assert_eq!( - result.events, - vec![ - emit_set_threshold(1u32, 8u8), - emit_set_threshold(2u32, 7u8), - emit_set_threshold(3u32, 6u8), - ] - ); - - // check it actually saved - for threshold in thresholds { - let saved_threshold = THRESHOLD.load(&deps.storage, threshold.domain).unwrap(); - assert_eq!(saved_threshold, threshold.threshold); - } - } - - #[rstest] - #[case("owner", vec![hex(&"deadbeef".repeat(5))])] - #[should_panic(expected = "unauthorized")] - #[case("someone", vec![hex(&"deadbeef".repeat(5))])] - #[should_panic(expected = "duplicate validator")] - #[case("owner", vec![hex(&"deadbeef".repeat(5)),hex(&"deadbeef".repeat(5))])] - fn test_enroll(#[case] sender: &str, #[case] validators: Vec) { - let mut deps = mock_dependencies(); - - hpl_ownable::initialize(deps.as_mut().storage, &addr("owner")).unwrap(); - - for validator in validators.clone() { - test_execute( - deps.as_mut(), - &addr(sender), - ExecuteMsg::EnrollValidator { - set: ValidatorSet { - domain: 1, - validator, - }, - }, - vec![], - ); - } - - assert_eq!( - VALIDATORS.load(deps.as_ref().storage, 1).unwrap(), - validators - ); - } - - #[rstest] - #[case("owner", hex("deadbeef"))] - #[should_panic(expected = "unauthorized")] - #[case("someone", hex("deadbeef"))] - #[should_panic(expected = "validator not exist")] - #[case("owner", hex("debeefed"))] - fn test_unenroll(#[case] sender: &str, #[case] target: HexBinary) { - let mut deps = mock_dependencies(); - - hpl_ownable::initialize(deps.as_mut().storage, &addr("owner")).unwrap(); - - VALIDATORS - .save(deps.as_mut().storage, 1, &vec![hex("deadbeef")]) - .unwrap(); - - test_execute( - deps.as_mut(), - &addr(sender), - ExecuteMsg::UnenrollValidator { - domain: 1, - validator: target, - }, - vec![], - ); - - assert!(VALIDATORS - .load(deps.as_ref().storage, 1) - .unwrap() - .is_empty()); - } -} From 1eb2687c3e1c8ea43a843e63883e47f583caae26 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Fri, 12 Jan 2024 14:46:19 -0500 Subject: [PATCH 07/15] Refactor mailbox hook fee logic --- contracts/core/mailbox/src/execute.rs | 74 +++++++++------------------ 1 file changed, 24 insertions(+), 50 deletions(-) diff --git a/contracts/core/mailbox/src/execute.rs b/contracts/core/mailbox/src/execute.rs index 3e43a62f..a1edef71 100644 --- a/contracts/core/mailbox/src/execute.rs +++ b/contracts/core/mailbox/src/execute.rs @@ -1,13 +1,14 @@ use cosmwasm_std::{ - ensure, ensure_eq, to_json_binary, wasm_execute, BankMsg, Coins, DepsMut, Env, HexBinary, - MessageInfo, Response, StdResult, + ensure, ensure_eq, has_coins, to_json_binary, wasm_execute, BankMsg, Coin, Coins, DepsMut, Env, + HexBinary, MessageInfo, Response, StdResult, }; +use cw_utils::PaymentError::MissingDenom; use hpl_interface::{ core::{ mailbox::{DispatchMsg, DispatchResponse}, HandleMsg, }, - hook::{self, post_dispatch}, + hook::{post_dispatch, quote_dispatch}, ism, types::Message, }; @@ -107,74 +108,47 @@ pub fn dispatch( } ); - // calculate gas - let default_hook = config.get_default_hook(); - let required_hook = config.get_required_hook(); - + // build hyperlane message let msg = dispatch_msg .clone() .to_msg(MAILBOX_VERSION, nonce, config.local_domain, &info.sender)?; - let msg_id = msg.id(); + let metadata = dispatch_msg.clone().metadata.unwrap_or_default(); + let hook = dispatch_msg.get_hook_addr(deps.api, config.get_default_hook())?; - let base_fee = hook::quote_dispatch( - &deps.querier, - dispatch_msg.get_hook_addr(deps.api, default_hook)?, - dispatch_msg.metadata.clone().unwrap_or_default(), - msg.clone(), - )? - .fees; - - let required_fee = hook::quote_dispatch( - &deps.querier, - &required_hook, - dispatch_msg.metadata.clone().unwrap_or_default(), - msg.clone(), - )? - .fees; - - // assert gas received is satisfies required gas - let mut total_fee = required_fee.clone().into_iter().try_fold( - Coins::try_from(base_fee.clone())?, - |mut acc, fee| { - acc.add(fee)?; - StdResult::Ok(acc) - }, - )?; - for fund in info.funds { - total_fee.sub(fund)?; - } + // assert gas received satisfies required gas + let required_hook = config.get_required_hook(); + let required_hook_fees: Vec = + quote_dispatch(&deps.querier, &required_hook, metadata.clone(), msg.clone())?.fees; - // interaction - let hook = dispatch_msg.get_hook_addr(deps.api, config.get_default_hook())?; - let hook_metadata = dispatch_msg.metadata.unwrap_or_default(); + let mut funds = Coins::try_from(info.funds)?; + for coin in required_hook_fees.iter() { + if let Err(_) = funds.sub(coin.clone()) { + return Err(ContractError::Payment(MissingDenom(coin.denom.clone()))); + } + } - // effects + // commit to message + let msg_id = msg.id(); NONCE.save(deps.storage, &(nonce + 1))?; LATEST_DISPATCHED_ID.save(deps.storage, &msg_id.to_vec())?; - // make message + // build post dispatch calls let post_dispatch_msgs = vec![ post_dispatch( required_hook, - hook_metadata.clone(), + metadata.clone(), msg.clone(), - Some(required_fee), + Some(required_hook_fees), )?, - post_dispatch(hook, hook_metadata, msg.clone(), Some(base_fee))?, + post_dispatch(hook, metadata, msg.clone(), Some(funds.to_vec()))?, ]; - let refund_msg = BankMsg::Send { - to_address: info.sender.to_string(), - amount: total_fee.to_vec(), - }; - Ok(Response::new() .add_event(emit_dispatch_id(msg_id.clone())) .add_event(emit_dispatch(msg)) .set_data(to_json_binary(&DispatchResponse { message_id: msg_id })?) - .add_messages(post_dispatch_msgs) - .add_message(refund_msg)) + .add_messages(post_dispatch_msgs)) } pub fn process( From bf30fe317c01957e4cc8457e940c5da3570c7ca0 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Fri, 12 Jan 2024 14:55:32 -0500 Subject: [PATCH 08/15] Remove unused imports --- contracts/core/mailbox/src/execute.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/core/mailbox/src/execute.rs b/contracts/core/mailbox/src/execute.rs index a1edef71..33792d39 100644 --- a/contracts/core/mailbox/src/execute.rs +++ b/contracts/core/mailbox/src/execute.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ - ensure, ensure_eq, has_coins, to_json_binary, wasm_execute, BankMsg, Coin, Coins, DepsMut, Env, - HexBinary, MessageInfo, Response, StdResult, + ensure, ensure_eq, to_json_binary, wasm_execute, Coin, Coins, DepsMut, Env, + HexBinary, MessageInfo, Response, }; use cw_utils::PaymentError::MissingDenom; use hpl_interface::{ From 25f1e46fa08d2aec588ca1ed5ca27a1b85086cd8 Mon Sep 17 00:00:00 2001 From: ByeongSu Hong Date: Tue, 16 Jan 2024 00:20:11 +0900 Subject: [PATCH 09/15] fix: coverage (#87) remove flag --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ae8bdc8f..780d7c8e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -52,7 +52,7 @@ jobs: uses: taiki-e/install-action@cargo-llvm-cov - name: Generate code coverage - run: cargo llvm-cov --all-features --workspace --exclude hpl-tests --codecov --output-path codecov.json + run: cargo llvm-cov --workspace --exclude hpl-tests --codecov --output-path codecov.json - name: Upload to codecov.io uses: codecov/codecov-action@v3 From f3b2896c1aa50dabae72354e47ab8c266a0858d1 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Tue, 16 Jan 2024 22:52:07 -0500 Subject: [PATCH 10/15] Make domain hash use left padded mailbox addresses (#88) * Left pad mailbox with zeroes in domain_hash * Add unit test for 20 byte mailbox --- contracts/core/va/src/contract.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/contracts/core/va/src/contract.rs b/contracts/core/va/src/contract.rs index 69bd6eb5..dd77b473 100644 --- a/contracts/core/va/src/contract.rs +++ b/contracts/core/va/src/contract.rs @@ -131,7 +131,10 @@ fn replay_hash(validator: &HexBinary, storage_location: &str) -> StdResult StdResult { let mut bz = vec![]; bz.append(&mut local_domain.to_be_bytes().to_vec()); - bz.append(&mut mailbox.to_vec()); + // left pad with zeroes + let mut addr = [0u8; 32]; + addr[32 - mailbox.len()..].copy_from_slice(&mailbox); + bz.append(&mut addr.to_vec()); bz.append(&mut "HYPERLANE_ANNOUNCEMENT".as_bytes().to_vec()); let hash = keccak256_hash(&bz); @@ -272,6 +275,12 @@ mod test { ) } + fn preset_20_byte_address() -> Self { + let mut p = Announcement::preset(); + p.mailbox = "49cfd6ef774acab14814d699e3f7ee36fdfba932".into(); + return p; + } + fn rand() -> Self { // prepare test data let mailbox = gen_bz(32); @@ -387,7 +396,8 @@ mod test { #[rstest] #[case::rand(Announcement::rand(), false)] - #[case::actual_data_1(Announcement::preset(), false)] + #[case::actual_data(Announcement::preset(), false)] + #[case::actual_data_20_bytes(Announcement::preset_20_byte_address(), false)] #[should_panic(expected = "unauthorized")] #[case::replay(Announcement::rand(), true)] #[should_panic(expected = "verify failed")] From 32e46b53323210d5cdc3dfbbca135c49ad088c04 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Tue, 23 Jan 2024 03:07:21 -0500 Subject: [PATCH 11/15] Implement simple protocol fee hook (#89) * Add pausable ism with tests * Fix paused query error case * Run CI against all PRs * Add pausable ISM to README * Build wasm * Fix scripts * Allow threshold == set size and add tests * Upload artifacts * Force * Move into makefile * Install rename * Rename properly * Update test.yaml * Implement simple fee hook_ * Make merkle hook not ownable * Address pr comments * Fix unit tests * Fix renaming * Fix makefile indentation * Force cargo install * Fix fee hook tests * Make set fee only owner * Implement remaining unit tests * Fix merkle integration test use --------- Co-authored-by: nambrot --- .github/workflows/test.yaml | 21 +- Cargo.toml | 1 + Makefile | 14 +- contracts/hooks/fee/Cargo.toml | 42 +++ contracts/hooks/fee/src/lib.rs | 275 ++++++++++++++++++++ contracts/hooks/merkle/src/lib.rs | 16 +- integration-test/tests/contracts/cw/hook.rs | 1 - packages/interface/src/hook/fee.rs | 83 ++++++ packages/interface/src/hook/merkle.rs | 5 - packages/interface/src/hook/mod.rs | 1 + 10 files changed, 432 insertions(+), 27 deletions(-) create mode 100644 contracts/hooks/fee/Cargo.toml create mode 100644 contracts/hooks/fee/src/lib.rs create mode 100644 packages/interface/src/hook/fee.rs diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 780d7c8e..1cbe220c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -28,12 +28,25 @@ jobs: key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Install Rust - run: rustup update stable + run: rustup update 1.72 - - name: Install target - run: rustup target add wasm32-unknown-unknown + - name: Install rename + run: sudo apt-get install -y rename + + - name: Install rust deps + run: make install - - run: cargo test --workspace --exclude hpl-tests + - name: Run tests + run: cargo test --workspace --exclude hpl-tests + + - name: Build wasm + run: make ci-build + + - name: Upload wasm archive + uses: actions/upload-artifact@v2 + with: + name: wasm_codes + path: wasm_codes.zip coverage: runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index 643fe642..9ef3ffa3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,6 +94,7 @@ hpl-mailbox = { path = "./contracts/core/mailbox" } hpl-validator-announce = { path = "./contracts/core/va" } hpl-hook-merkle = { path = "./contracts/hooks/merkle" } +hpl-hook-fee = { path = "./contracts/hooks/fee" } hpl-hook-pausable = { path = "./contracts/hooks/pausable" } hpl-hook-routing = { path = "./contracts/hooks/routing" } hpl-hook-routing-custom = { path = "./contracts/hooks/routing-custom" } diff --git a/Makefile b/Makefile index fa2176ef..a7b1cf3e 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,22 @@ - clean: @cargo clean @rm -rf ./artifacts +install: + cargo install --force cw-optimizoor cosmwasm-check beaker + rustup target add wasm32-unknown-unknown + schema: ls ./contracts | xargs -n 1 -t beaker wasm ts-gen build: + cargo build cargo wasm - -build-dev: clean cargo cw-optimizoor + rename --force 's/(.*)-(.*)\.wasm/$$1\.wasm/d' artifacts/* -check: build-dev +check: build ls -d ./artifacts/*.wasm | xargs -I x cosmwasm-check x + +ci-build: check + zip -jr wasm_codes.zip artifacts diff --git a/contracts/hooks/fee/Cargo.toml b/contracts/hooks/fee/Cargo.toml new file mode 100644 index 00000000..d5f6e792 --- /dev/null +++ b/contracts/hooks/fee/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "hpl-hook-fee" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +keywords.workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-std.workspace = true +cosmwasm-storage.workspace = true +cosmwasm-schema.workspace = true + +cw-storage-plus.workspace = true +cw2.workspace = true +cw-utils.workspace = true + +schemars.workspace = true +serde-json-wasm.workspace = true + +thiserror.workspace = true + +hpl-ownable.workspace = true +hpl-interface.workspace = true + +[dev-dependencies] +rstest.workspace = true +ibcx-test-utils.workspace = true + +anyhow.workspace = true diff --git a/contracts/hooks/fee/src/lib.rs b/contracts/hooks/fee/src/lib.rs new file mode 100644 index 00000000..743d6e79 --- /dev/null +++ b/contracts/hooks/fee/src/lib.rs @@ -0,0 +1,275 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + ensure, ensure_eq, BankMsg, Coin, CosmosMsg, Deps, DepsMut, Env, Event, MessageInfo, + QueryResponse, Response, StdError, +}; +use cw_storage_plus::Item; +use hpl_interface::{ + hook::{ + fee::{ExecuteMsg, FeeHookMsg, FeeHookQueryMsg, FeeResponse, InstantiateMsg, QueryMsg}, + HookQueryMsg, MailboxResponse, QuoteDispatchResponse, + }, + to_binary, +}; + +#[derive(thiserror::Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + PaymentError(#[from] cw_utils::PaymentError), + + #[error("unauthorized")] + Unauthorized {}, + + #[error("hook paused")] + Paused {}, +} + +// version info for migration info +pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub const COIN_FEE_KEY: &str = "coin_fee"; +pub const COIN_FEE: Item = Item::new(COIN_FEE_KEY); + +fn new_event(name: &str) -> Event { + Event::new(format!("hpl_hook_fee::{}", name)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + let owner = deps.api.addr_validate(&msg.owner)?; + + hpl_ownable::initialize(deps.storage, &owner)?; + COIN_FEE.save(deps.storage, &msg.fee)?; + + Ok(Response::new().add_event( + new_event("initialize") + .add_attribute("sender", info.sender) + .add_attribute("owner", owner) + .add_attribute("fee_denom", msg.fee.denom) + .add_attribute("fee_amount", msg.fee.amount), + )) +} + +fn get_fee(deps: Deps) -> Result { + let fee = COIN_FEE.load(deps.storage)?; + + Ok(FeeResponse { fee }) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Ownable(msg) => Ok(hpl_ownable::handle(deps, env, info, msg)?), + ExecuteMsg::FeeHook(msg) => match msg { + FeeHookMsg::SetFee { fee } => { + let owner = hpl_ownable::get_owner(deps.storage)?; + ensure_eq!(owner, info.sender, StdError::generic_err("unauthorized")); + + COIN_FEE.save(deps.storage, &fee)?; + + Ok(Response::new().add_event( + new_event("set_fee") + .add_attribute("fee_denom", fee.denom) + .add_attribute("fee_amount", fee.amount), + )) + } + FeeHookMsg::Claim { recipient } => { + let owner = hpl_ownable::get_owner(deps.storage)?; + ensure_eq!(owner, info.sender, StdError::generic_err("unauthorized")); + + let recipient = recipient.unwrap_or(owner); + let balances = deps.querier.query_all_balances(&env.contract.address)?; + + let claim_msg: CosmosMsg = BankMsg::Send { + to_address: recipient.into_string(), + amount: balances, + } + .into(); + + Ok(Response::new() + .add_message(claim_msg) + .add_event(new_event("claim"))) + } + }, + ExecuteMsg::PostDispatch(_) => { + let fee = COIN_FEE.load(deps.storage)?; + let supplied = cw_utils::must_pay(&info, &fee.denom)?; + + ensure!( + supplied.u128() >= fee.amount.u128(), + // TODO: improve error + StdError::generic_err("insufficient funds") + ); + + Ok(Response::new().add_event( + new_event("post_dispatch") + .add_attribute("paid_denom", fee.denom) + .add_attribute("paid_amount", supplied), + )) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { + match msg { + QueryMsg::Ownable(msg) => Ok(hpl_ownable::handle_query(deps, env, msg)?), + QueryMsg::Hook(msg) => match msg { + HookQueryMsg::Mailbox {} => to_binary(get_mailbox(deps)), + HookQueryMsg::QuoteDispatch(_) => to_binary(quote_dispatch(deps)), + }, + QueryMsg::FeeHook(FeeHookQueryMsg::Fee {}) => to_binary(get_fee(deps)), + } +} + +fn get_mailbox(_deps: Deps) -> Result { + Ok(MailboxResponse { + mailbox: "unrestricted".to_string(), + }) +} + +fn quote_dispatch(deps: Deps) -> Result { + let fee = COIN_FEE.load(deps.storage)?; + Ok(QuoteDispatchResponse { fees: vec![fee] }) +} + +#[cfg(test)] +mod test { + use cosmwasm_schema::serde::{de::DeserializeOwned, Serialize}; + use cosmwasm_std::{ + coin, from_json, + testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage}, + to_json_binary, Addr, HexBinary, OwnedDeps, + }; + use hpl_interface::hook::{PostDispatchMsg, QuoteDispatchMsg}; + use hpl_ownable::get_owner; + use ibcx_test_utils::{addr, gen_bz}; + use rstest::{fixture, rstest}; + + use super::*; + + type TestDeps = OwnedDeps; + + fn query(deps: Deps, msg: S) -> T { + let req: QueryMsg = from_json(to_json_binary(&msg).unwrap()).unwrap(); + let res = crate::query(deps, mock_env(), req).unwrap(); + from_json(res).unwrap() + } + + #[fixture] + fn deps( + #[default(addr("deployer"))] sender: Addr, + #[default(addr("owner"))] owner: Addr, + #[default(coin(100, "uusd"))] fee: Coin, + ) -> TestDeps { + let mut deps = mock_dependencies(); + + instantiate( + deps.as_mut(), + mock_env(), + mock_info(sender.as_str(), &[]), + InstantiateMsg { + owner: owner.to_string(), + fee, + }, + ) + .unwrap(); + + deps + } + + #[rstest] + fn test_init(deps: TestDeps) { + assert_eq!("uusd", get_fee(deps.as_ref()).unwrap().fee.denom.as_str()); + assert_eq!("owner", get_owner(deps.as_ref().storage).unwrap().as_str()); + } + + #[rstest] + #[case(&[coin(100, "uusd")])] + #[should_panic(expected = "Generic error: insufficient funds")] + #[case(&[coin(99, "uusd")])] + fn test_post_dispatch(mut deps: TestDeps, #[case] funds: &[Coin]) { + execute( + deps.as_mut(), + mock_env(), + mock_info("owner", funds), + ExecuteMsg::PostDispatch(PostDispatchMsg { + metadata: HexBinary::default(), + message: gen_bz(100), + }), + ) + .map_err(|e| e.to_string()) + .unwrap(); + } + + #[rstest] + fn test_query(deps: TestDeps) { + let res: MailboxResponse = query(deps.as_ref(), QueryMsg::Hook(HookQueryMsg::Mailbox {})); + assert_eq!("unrestricted", res.mailbox.as_str()); + + let res: QuoteDispatchResponse = query( + deps.as_ref(), + QueryMsg::Hook(HookQueryMsg::QuoteDispatch(QuoteDispatchMsg::default())), + ); + assert_eq!(res.fees, vec![coin(100, "uusd")]); + } + + #[rstest] + #[case(addr("owner"), coin(200, "uusd"))] + #[should_panic(expected = "unauthorized")] + #[case(addr("deployer"), coin(200, "uusd"))] + fn test_set_fee(mut deps: TestDeps, #[case] sender: Addr, #[case] fee: Coin) { + execute( + deps.as_mut(), + mock_env(), + mock_info(sender.as_str(), &[]), + ExecuteMsg::FeeHook(FeeHookMsg::SetFee { fee: fee.clone() }), + ) + .map_err(|e| e.to_string()) + .unwrap(); + + assert_eq!(fee, get_fee(deps.as_ref()).unwrap().fee); + } + + #[rstest] + #[case(addr("owner"), Some(addr("deployer")))] + #[case(addr("owner"), None)] + #[should_panic(expected = "unauthorized")] + #[case(addr("deployer"), None)] + fn test_claim(mut deps: TestDeps, #[case] sender: Addr, #[case] recipient: Option) { + let res = execute( + deps.as_mut(), + mock_env(), + mock_info(sender.as_str(), &[]), + ExecuteMsg::FeeHook(FeeHookMsg::Claim { recipient: recipient.clone() }), + ) + .map_err(|e| e.to_string()) + .unwrap(); + + assert_eq!( + CosmosMsg::Bank(BankMsg::Send { + to_address: recipient.unwrap_or_else(|| addr("owner")).into_string(), + amount: vec![], + }), + res.messages[0].msg + ); + println!("{:?}", res); + } +} diff --git a/contracts/hooks/merkle/src/lib.rs b/contracts/hooks/merkle/src/lib.rs index e837cb83..be13388a 100644 --- a/contracts/hooks/merkle/src/lib.rs +++ b/contracts/hooks/merkle/src/lib.rs @@ -59,18 +59,14 @@ pub fn instantiate( ) -> Result { cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - let owner = deps.api.addr_validate(&msg.owner)?; let mailbox = deps.api.addr_validate(&msg.mailbox)?; - hpl_ownable::initialize(deps.storage, &owner)?; - MAILBOX.save(deps.storage, &mailbox)?; MESSAGE_TREE.save(deps.storage, &MerkleTree::default())?; Ok(Response::new().add_event( new_event("initialize") .add_attribute("sender", info.sender) - .add_attribute("owner", owner) .add_attribute("mailbox", mailbox), )) } @@ -78,12 +74,11 @@ pub fn instantiate( #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( deps: DepsMut, - env: Env, - info: MessageInfo, + _env: Env, + _info: MessageInfo, msg: ExecuteMsg, ) -> Result { match msg { - ExecuteMsg::Ownable(msg) => Ok(hpl_ownable::handle(deps, env, info, msg)?), ExecuteMsg::PostDispatch(PostDispatchMsg { message, .. }) => { let mailbox = MAILBOX.load(deps.storage)?; @@ -123,11 +118,10 @@ pub fn execute( } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { use MerkleHookQueryMsg::*; match msg { - QueryMsg::Ownable(msg) => Ok(hpl_ownable::handle_query(deps, env, msg)?), QueryMsg::Hook(msg) => match msg { HookQueryMsg::Mailbox {} => to_binary(get_mailbox(deps)), HookQueryMsg::QuoteDispatch(_) => to_binary(quote_dispatch()), @@ -214,7 +208,6 @@ mod test { use hpl_interface::{ build_test_executor, build_test_querier, core::mailbox, hook::QuoteDispatchMsg, }; - use hpl_ownable::get_owner; use ibcx_test_utils::hex; use rstest::{fixture, rstest}; @@ -228,7 +221,6 @@ mod test { #[fixture] fn deps( #[default(Addr::unchecked("deployer"))] sender: Addr, - #[default(Addr::unchecked("owner"))] owner: Addr, #[default(Addr::unchecked("mailbox"))] mailbox: Addr, ) -> TestDeps { let mut deps = mock_dependencies(); @@ -238,7 +230,6 @@ mod test { mock_env(), mock_info(sender.as_str(), &[]), InstantiateMsg { - owner: owner.to_string(), mailbox: mailbox.to_string(), }, ) @@ -249,7 +240,6 @@ mod test { #[rstest] fn test_init(deps: TestDeps) { - assert_eq!("owner", get_owner(deps.as_ref().storage).unwrap().as_str()); assert_eq!( "mailbox", MAILBOX.load(deps.as_ref().storage).unwrap().as_str() diff --git a/integration-test/tests/contracts/cw/hook.rs b/integration-test/tests/contracts/cw/hook.rs index c7d4cfb3..413c8cd2 100644 --- a/integration-test/tests/contracts/cw/hook.rs +++ b/integration-test/tests/contracts/cw/hook.rs @@ -99,7 +99,6 @@ impl Hook { .instantiate( codes.hook_merkle, &hook::merkle::InstantiateMsg { - owner: owner.address(), mailbox, }, Some(deployer.address().as_str()), diff --git a/packages/interface/src/hook/fee.rs b/packages/interface/src/hook/fee.rs new file mode 100644 index 00000000..b814b642 --- /dev/null +++ b/packages/interface/src/hook/fee.rs @@ -0,0 +1,83 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Coin}; + +use crate::ownable::{OwnableMsg, OwnableQueryMsg}; + +use super::{HookQueryMsg, PostDispatchMsg}; + +pub const TREE_DEPTH: usize = 32; + +#[cw_serde] +pub struct InstantiateMsg { + pub owner: String, + pub fee: Coin, +} + +#[cw_serde] +pub enum ExecuteMsg { + Ownable(OwnableMsg), + PostDispatch(PostDispatchMsg), + FeeHook(FeeHookMsg), +} + +#[cw_serde] +pub enum FeeHookMsg { + SetFee { + fee: Coin, + }, + Claim { + recipient: Option + } +} + +#[cw_serde] +#[derive(QueryResponses)] +#[query_responses(nested)] +pub enum QueryMsg { + Ownable(OwnableQueryMsg), + Hook(HookQueryMsg), + FeeHook(FeeHookQueryMsg), +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum FeeHookQueryMsg { + #[returns(FeeResponse)] + Fee {} +} + +#[cw_serde] +pub struct FeeResponse { + pub fee: Coin, +} + +#[cfg(test)] +mod test { + use cosmwasm_std::HexBinary; + + use super::*; + use crate::{ + hook::{ExpectedHookQueryMsg, PostDispatchMsg, QuoteDispatchMsg}, + msg_checker, + }; + + #[test] + fn test_hook_interface() { + let _checked: ExecuteMsg = msg_checker( + PostDispatchMsg { + metadata: HexBinary::default(), + message: HexBinary::default(), + } + .wrap(), + ); + + let _checked: QueryMsg = msg_checker(ExpectedHookQueryMsg::Hook(HookQueryMsg::Mailbox {})); + let _checked: QueryMsg = msg_checker( + QuoteDispatchMsg { + metadata: HexBinary::default(), + message: HexBinary::default(), + } + .request(), + ); + } +} diff --git a/packages/interface/src/hook/merkle.rs b/packages/interface/src/hook/merkle.rs index 007ac839..85d6180d 100644 --- a/packages/interface/src/hook/merkle.rs +++ b/packages/interface/src/hook/merkle.rs @@ -1,21 +1,17 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::HexBinary; -use crate::ownable::{OwnableMsg, OwnableQueryMsg}; - use super::{HookQueryMsg, PostDispatchMsg}; pub const TREE_DEPTH: usize = 32; #[cw_serde] pub struct InstantiateMsg { - pub owner: String, pub mailbox: String, } #[cw_serde] pub enum ExecuteMsg { - Ownable(OwnableMsg), PostDispatch(PostDispatchMsg), } @@ -23,7 +19,6 @@ pub enum ExecuteMsg { #[derive(QueryResponses)] #[query_responses(nested)] pub enum QueryMsg { - Ownable(OwnableQueryMsg), Hook(HookQueryMsg), MerkleHook(MerkleHookQueryMsg), } diff --git a/packages/interface/src/hook/mod.rs b/packages/interface/src/hook/mod.rs index 0c368b0b..1c76d998 100644 --- a/packages/interface/src/hook/mod.rs +++ b/packages/interface/src/hook/mod.rs @@ -4,6 +4,7 @@ pub mod pausable; pub mod routing; pub mod routing_custom; pub mod routing_fallback; +pub mod fee; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{ From aa5539e706ccb02beaddf73ad0e79bf4e6ac5500 Mon Sep 17 00:00:00 2001 From: ByeongSu Hong Date: Tue, 23 Jan 2024 17:07:30 +0900 Subject: [PATCH 12/15] fix: remove redundant protobuf (#90) remove redundant protobuf --- contracts/igps/core/src/lib.rs | 1 - contracts/igps/core/src/proto.rs | 24 ------------------------ 2 files changed, 25 deletions(-) delete mode 100644 contracts/igps/core/src/proto.rs diff --git a/contracts/igps/core/src/lib.rs b/contracts/igps/core/src/lib.rs index c45aa050..6085f7f3 100644 --- a/contracts/igps/core/src/lib.rs +++ b/contracts/igps/core/src/lib.rs @@ -2,7 +2,6 @@ pub mod contract; mod error; mod event; pub mod execute; -mod proto; pub mod query; #[cfg(test)] diff --git a/contracts/igps/core/src/proto.rs b/contracts/igps/core/src/proto.rs deleted file mode 100644 index 13bd68d0..00000000 --- a/contracts/igps/core/src/proto.rs +++ /dev/null @@ -1,24 +0,0 @@ -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(::prost::Message, ::serde::Serialize, ::serde::Deserialize)] -pub struct QuerySupplyOfRequest { - /// denom is the coin denom to query balances for. - #[prost(string, tag = "1")] - pub denom: ::prost::alloc::string::String, -} - -/// QuerySupplyOfResponse is the response type for the Query/SupplyOf RPC method. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(::prost::Message, ::serde::Serialize, ::serde::Deserialize)] -pub struct QuerySupplyOfResponse { - /// amount is the supply of the coin. - #[prost(message, optional, tag = "1")] - pub amount: ::core::option::Option, -} - -#[derive(serde::Serialize, serde::Deserialize, ::prost::Message)] -pub struct Coin { - #[prost(string, tag = "1")] - pub denom: ::prost::alloc::string::String, - #[prost(string, tag = "2")] - pub amount: ::prost::alloc::string::String, -} From e5bfbba828458f3177f8e9fe16ae2233507e646b Mon Sep 17 00:00:00 2001 From: ByeongSu Hong Date: Thu, 25 Jan 2024 15:45:49 +0900 Subject: [PATCH 13/15] feat: use optimizer (#92) * optimizer * remove deps installation * revive * pr * setup go * bump test-tube * fix cov * specify artifacts branch * split artifact generation * fix --- .github/workflows/test.yaml | 49 +++++++++++++++++++++++++++++-------- Cargo.toml | 4 +-- Makefile | 10 ++++++-- 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1cbe220c..ef8008c1 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,12 +10,16 @@ jobs: unit-test: strategy: fail-fast: true - + name: unit-test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 + with: + go-version: '1.21' + - name: Cache dependencies uses: actions/cache@v3 with: @@ -39,14 +43,36 @@ jobs: - name: Run tests run: cargo test --workspace --exclude hpl-tests + artifact: + permissions: + contents: write + pull-requests: write + + name: artifact + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Dependencies + run: | + rustup update 1.72 + sudo apt-get install -y rename + make install + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Build wasm run: make ci-build - - name: Upload wasm archive - uses: actions/upload-artifact@v2 + - name: Pull request artifacts + uses: gavv/pull-request-artifacts@v2 with: - name: wasm_codes - path: wasm_codes.zip + commit: ${{ github.event.pull_request.head.sha }} + repo-token: ${{ secrets.GITHUB_TOKEN }} + artifacts-branch: artifacts + artifacts: | + wasm_codes.zip coverage: runs-on: ubuntu-latest @@ -54,12 +80,15 @@ jobs: CARGO_TERM_COLOR: always steps: - uses: actions/checkout@v4 - + + - uses: actions/setup-go@v4 + with: + go-version: '1.21' + - name: Install Rust - run: rustup update nightly - - - name: Install target - run: rustup target add wasm32-unknown-unknown + run: | + rustup update nightly + rustup target add wasm32-unknown-unknown - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov diff --git a/Cargo.toml b/Cargo.toml index 9ef3ffa3..7fa7f06c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,8 +82,8 @@ digest = { version = "0.10.7" } # testing cw-multi-test = "0.20.0" rstest = "0.18.2" -test-tube = { version = "0.3.0" } -osmosis-test-tube = { version = "21.0.0" } +test-tube = { version = "0.5.0" } +osmosis-test-tube = { version = "22.1.0" } ibcx-test-utils = { version = "0.1.2" } tokio = { version = "1", features = ["full"] } diff --git a/Makefile b/Makefile index a7b1cf3e..31bcc6cf 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,12 @@ +PWD:=$(shell pwd) +BASE:=$(shell basename "$(PWD)") + clean: @cargo clean @rm -rf ./artifacts install: - cargo install --force cw-optimizoor cosmwasm-check beaker + cargo install --force cosmwasm-check rustup target add wasm32-unknown-unknown schema: @@ -12,7 +15,10 @@ schema: build: cargo build cargo wasm - cargo cw-optimizoor + docker run --rm -v "$(PWD)":/code \ + --mount type=volume,source="$(BASE)_cache",target=/code/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + cosmwasm/optimizer:0.15.0 rename --force 's/(.*)-(.*)\.wasm/$$1\.wasm/d' artifacts/* check: build From 659594588d78b1a32d644b903fc6bf321a9b632d Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Thu, 25 Jan 2024 01:52:18 -0500 Subject: [PATCH 14/15] Add mailbox unit tests for post dispatch (#91) * Add pausable ism with tests * Fix paused query error case * Run CI against all PRs * Add pausable ISM to README * Build wasm * Fix scripts * Allow threshold == set size and add tests * Upload artifacts * Force * Move into makefile * Install rename * Rename properly * Update test.yaml * Fix renaming * Fix makefile indentation * Force cargo install * simple fee hook (#6) * Implement simple fee hook * Address pr comments * Fix unit tests * Make set fee only owner * Implement remaining unit tests * Fix merkle integration test use --------- Co-authored-by: nambrot Co-authored-by: ByeongSu Hong * Add mailbox unit tests for post dispatch (#7) * Add mailbox unit tests for post dispatch * Add test for different denoms --------- Co-authored-by: nambrot Co-authored-by: ByeongSu Hong --- contracts/core/mailbox/src/error.rs | 8 +- contracts/core/mailbox/src/execute.rs | 130 +++++++++++++++++++++++--- 2 files changed, 125 insertions(+), 13 deletions(-) diff --git a/contracts/core/mailbox/src/error.rs b/contracts/core/mailbox/src/error.rs index 18d56746..8df21fca 100644 --- a/contracts/core/mailbox/src/error.rs +++ b/contracts/core/mailbox/src/error.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::StdError; +use cosmwasm_std::{Coin, StdError}; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -9,6 +9,12 @@ pub enum ContractError { #[error("{0}")] Payment(#[from] cw_utils::PaymentError), + #[error("insufficient hook payment: wanted {wanted:?}, received {received:?}")] + HookPayment { + wanted: Vec, + received: Vec, + }, + #[error("{0}")] CoinsError(#[from] cosmwasm_std::CoinsError), diff --git a/contracts/core/mailbox/src/execute.rs b/contracts/core/mailbox/src/execute.rs index 33792d39..5f06e026 100644 --- a/contracts/core/mailbox/src/execute.rs +++ b/contracts/core/mailbox/src/execute.rs @@ -121,10 +121,13 @@ pub fn dispatch( let required_hook_fees: Vec = quote_dispatch(&deps.querier, &required_hook, metadata.clone(), msg.clone())?.fees; - let mut funds = Coins::try_from(info.funds)?; + let mut funds = Coins::try_from(info.funds.clone())?; for coin in required_hook_fees.iter() { if let Err(_) = funds.sub(coin.clone()) { - return Err(ContractError::Payment(MissingDenom(coin.denom.clone()))); + return Err(ContractError::HookPayment { + wanted: required_hook_fees, + received: info.funds, + }); } } @@ -225,15 +228,18 @@ pub fn process( #[cfg(test)] mod tests { + use std::collections::HashMap; + use cosmwasm_std::{ coin, from_json, testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage}, - to_json_binary, Addr, ContractResult, OwnedDeps, QuerierResult, SystemResult, WasmQuery, + to_json_binary, Addr, ContractResult, CosmosMsg, OwnedDeps, QuerierResult, + SystemResult, WasmMsg, WasmQuery, }; use hpl_interface::{ core::mailbox::InstantiateMsg, - hook::{ExpectedHookQueryMsg, HookQueryMsg, QuoteDispatchResponse}, + hook::{ExpectedHookQueryMsg, HookQueryMsg, PostDispatchMsg, QuoteDispatchResponse}, ism::IsmQueryMsg, types::bech32_encode, }; @@ -252,8 +258,11 @@ mod tests { type TestDeps = OwnedDeps; - fn mock_query_handler(req: &WasmQuery) -> QuerierResult { - let (req, _addr) = match req { + fn mock_query_handler( + req: &WasmQuery, + addr_fees: &Option>>, + ) -> QuerierResult { + let (req, addr) = match req { WasmQuery::Smart { msg, contract_addr } => (from_json(msg).unwrap(), contract_addr), _ => unreachable!("wrong query type"), }; @@ -263,17 +272,18 @@ mod tests { _ => unreachable!("wrong query type"), }; - let mut fees = Coins::default(); + let mut fees = match addr_fees { + Some(fees) => fees.get(addr).unwrap_or(&vec![]).clone(), + None => vec![], + }; if !req.metadata.is_empty() { let parsed_fee = u32::from_be_bytes(req.metadata.as_slice().try_into().unwrap()); - fees = Coins::from(coin(parsed_fee as u128, "utest")); + fees = vec![coin(parsed_fee as u128, "utest")]; } - let res = QuoteDispatchResponse { - fees: fees.into_vec(), - }; + let res = QuoteDispatchResponse { fees }; let res = to_json_binary(&res).unwrap(); SystemResult::Ok(ContractResult::Ok(res)) @@ -396,7 +406,7 @@ mod tests { let mut deps = mock_dependencies(); - deps.querier.update_wasm(mock_query_handler); + deps.querier.update_wasm(|q| mock_query_handler(q, &None)); instantiate( deps.as_mut(), @@ -442,6 +452,102 @@ mod tests { ); } + #[rstest] + #[case(vec![coin(100, "usd")], vec![coin(100, "usd")])] + #[should_panic] + #[case(vec![coin(100, "usd")], vec![coin(50, "usd")])] + #[should_panic] + #[case(vec![coin(100, "usdt")], vec![coin(100, "usd")])] + #[case(vec![coin(50, "usd")], vec![coin(100, "usd")])] + fn test_post_dispatch(#[case] required_hook_fees: Vec, #[case] funds: Vec) { + let mut deps = mock_dependencies(); + + let mut hook_fees = HashMap::new(); + hook_fees.insert("required_hook".into(), required_hook_fees.clone()); + + // not enforced by mailbox + // hook_fees.insert("default_hook".into(), default_hook_fees); + + let opt = Some(hook_fees); + + deps.querier + .update_wasm(move |q| mock_query_handler(q, &opt)); + + let hrp = "osmo"; + + instantiate( + deps.as_mut(), + mock_env(), + mock_info(OWNER, &[]), + InstantiateMsg { + hrp: "osmo".to_string(), + owner: OWNER.to_string(), + domain: LOCAL_DOMAIN, + }, + ) + .unwrap(); + + set_default_hook(deps.as_mut(), mock_info(OWNER, &[]), "default_hook".into()).unwrap(); + set_required_hook(deps.as_mut(), mock_info(OWNER, &[]), "required_hook".into()).unwrap(); + + let dispatch_msg = DispatchMsg::new(DEST_DOMAIN, gen_bz(32), gen_bz(123)); + + let sender = bech32_encode(hrp, gen_bz(32).as_slice()).unwrap(); + + let msg = dispatch_msg + .clone() + .to_msg( + MAILBOX_VERSION, + NONCE.load(deps.as_ref().storage).unwrap(), + LOCAL_DOMAIN, + &sender, + ) + .unwrap(); + + let post_dispatch_msg = to_json_binary( + &PostDispatchMsg { + metadata: HexBinary::default(), + message: msg.into(), + } + .wrap(), // not sure why I need this + ) + .unwrap(); + + let res = dispatch( + deps.as_mut(), + mock_info(sender.as_str(), &funds), + dispatch_msg.clone(), + ) + .map_err(|e| e.to_string()) + .unwrap(); + + let msgs: Vec<_> = res.messages.into_iter().map(|v| v.msg).collect(); + + assert_eq!( + msgs[0], + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "required_hook".to_string(), + msg: post_dispatch_msg.clone(), + funds: required_hook_fees.clone() + },) + ); + + // subtract required_hook_fees from funds + let mut remaining_funds = Coins::try_from(funds).unwrap(); + for coin in required_hook_fees { + remaining_funds.sub(coin).unwrap(); + } + + assert_eq!( + msgs[1], + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "default_hook".to_string(), + msg: post_dispatch_msg, + funds: remaining_funds.into_vec() // forward all remaining funds + }) + ); + } + fn test_process_query_handler(query: &WasmQuery) -> QuerierResult { match query { WasmQuery::Smart { contract_addr, msg } => { From d1979e9ab21dfddb1f2b316b1a3177b62aec073f Mon Sep 17 00:00:00 2001 From: ByeongSu Hong Date: Thu, 25 Jan 2024 17:14:45 +0900 Subject: [PATCH 15/15] fix: pr and test (#93) * pr and test * new cache * generate lockfile * apply cache * rename --- .github/workflows/pr.yaml | 47 +++++++++++++++++++++++++++ .github/workflows/test.yaml | 64 ++++++++----------------------------- Makefile | 24 ++++++++++---- 3 files changed, 78 insertions(+), 57 deletions(-) create mode 100644 .github/workflows/pr.yaml diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml new file mode 100644 index 00000000..76dcd7b9 --- /dev/null +++ b/.github/workflows/pr.yaml @@ -0,0 +1,47 @@ +name: pr + +on: + pull_request: + branches: + - "main" + +jobs: + artifact: + permissions: + contents: write + pull-requests: write + + name: artifact + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + run: | + rustup toolchain install 1.72 \ + --profile minimal \ + --target wasm32-unknown-unknown \ + --no-self-update + + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 + + - name: Install Deps + run: make install-prod + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build wasm + run: | + cargo generate-lockfile + make ci-build + + - name: Pull request artifacts + uses: gavv/pull-request-artifacts@v2 + with: + commit: ${{ github.event.pull_request.head.sha }} + repo-token: ${{ secrets.GITHUB_TOKEN }} + artifacts-branch: artifacts + artifacts: | + wasm_codes.zip diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ef8008c1..6a4c00a6 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -20,60 +20,19 @@ jobs: with: go-version: '1.21' - - name: Cache dependencies - uses: actions/cache@v3 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: Install Rust - run: rustup update 1.72 - - - name: Install rename - run: sudo apt-get install -y rename + run: | + rustup toolchain install 1.72 \ + --profile minimal \ + --target wasm32-unknown-unknown \ + --no-self-update - - name: Install rust deps - run: make install + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 - name: Run tests run: cargo test --workspace --exclude hpl-tests - artifact: - permissions: - contents: write - pull-requests: write - - name: artifact - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Dependencies - run: | - rustup update 1.72 - sudo apt-get install -y rename - make install - - - name: Setup Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Build wasm - run: make ci-build - - - name: Pull request artifacts - uses: gavv/pull-request-artifacts@v2 - with: - commit: ${{ github.event.pull_request.head.sha }} - repo-token: ${{ secrets.GITHUB_TOKEN }} - artifacts-branch: artifacts - artifacts: | - wasm_codes.zip - coverage: runs-on: ubuntu-latest env: @@ -87,8 +46,13 @@ jobs: - name: Install Rust run: | - rustup update nightly - rustup target add wasm32-unknown-unknown + rustup toolchain install 1.72 \ + --profile minimal \ + --target wasm32-unknown-unknown \ + --no-self-update + + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov diff --git a/Makefile b/Makefile index 31bcc6cf..b1864dc3 100644 --- a/Makefile +++ b/Makefile @@ -5,24 +5,34 @@ clean: @cargo clean @rm -rf ./artifacts -install: +install: install-dev + +install-dev: install-prod + cargo install --force cw-optimizoor beaker + +install-prod: cargo install --force cosmwasm-check rustup target add wasm32-unknown-unknown schema: ls ./contracts | xargs -n 1 -t beaker wasm ts-gen -build: - cargo build - cargo wasm +check: + ls -d ./artifacts/*.wasm | xargs -I x cosmwasm-check x + +optimize: docker run --rm -v "$(PWD)":/code \ --mount type=volume,source="$(BASE)_cache",target=/code/target \ --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ cosmwasm/optimizer:0.15.0 + +optimize-fast: + cargo cw-optimizoor rename --force 's/(.*)-(.*)\.wasm/$$1\.wasm/d' artifacts/* -check: build - ls -d ./artifacts/*.wasm | xargs -I x cosmwasm-check x +build: optimize-fast check + cargo build + cargo wasm -ci-build: check +ci-build: optimize check zip -jr wasm_codes.zip artifacts