diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f159d439..7959e3cd 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -9,50 +9,14 @@ on: - "main" jobs: - deps: - name: dependencies - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - with: - fetch-depth: 1 - - - 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: Load toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: stable - targets: wasm32-unknown-unknown - - - run: cargo fetch --verbose - - run: cargo build - - run: cargo wasm - unit-test: strategy: fail-fast: true - needs: deps - name: unit-test runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v3 - with: - fetch-depth: 1 + - uses: actions/checkout@v4 - name: Cache dependencies uses: actions/cache@v3 @@ -65,45 +29,32 @@ jobs: target/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: Load toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: stable - targets: wasm32-unknown-unknown + - name: Install Rust + run: rustup update stable + + - name: Install target + run: rustup target add wasm32-unknown-unknown - run: cargo test --workspace --exclude hpl-tests coverage: - needs: deps - - name: coverage runs-on: ubuntu-latest - container: - image: xd009642/tarpaulin:develop-nightly - options: --security-opt seccomp=unconfined + env: + CARGO_TERM_COLOR: always steps: - - name: Checkout repository - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - 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 nightly + + - name: Install target + run: rustup target add wasm32-unknown-unknown - - run: rustup target add wasm32-unknown-unknown + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov - name: Generate code coverage - run: | - cargo +nightly tarpaulin \ - --verbose \ - --workspace --exclude hpl-tests \ - --timeout 120 --out Xml + run: cargo llvm-cov --all-features --workspace --exclude hpl-tests --codecov --output-path codecov.json - name: Upload to codecov.io uses: codecov/codecov-action@v3 diff --git a/Cargo.toml b/Cargo.toml index fed4bf5f..283425bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ panic = "abort" rpath = false [workspace.package] -version = "0.0.6-rc1" +version = "0.0.6-rc6" authors = [ "byeongsu-hong ", "Eric ", @@ -42,7 +42,7 @@ keywords = ["hyperlane", "cosmos", "cosmwasm"] [workspace.dependencies] # cosmwasm -cosmwasm-std = { version = "1.2.7", features = ["stargate", "cosmwasm_1_2"] } +cosmwasm-std = { version = "1.2.7", features = ["stargate"] } cosmwasm-storage = "1.2.7" cosmwasm-schema = "1.2.7" cosmwasm-crypto = "1.2.7" @@ -109,6 +109,7 @@ hpl-warp-cw20 = { path = "./contracts/warp/cw20" } hpl-warp-native = { path = "./contracts/warp/native" } # workspace aliases (./packages) +hpl-connection = { path = "./packages/connection" } hpl-ownable = { path = "./packages/ownable" } hpl-pausable = { path = "./packages/pausable" } hpl-router = { path = "./packages/router" } diff --git a/contracts/core/mailbox/src/contract.rs b/contracts/core/mailbox/src/contract.rs index 306cafe3..e74ca14f 100644 --- a/contracts/core/mailbox/src/contract.rs +++ b/contracts/core/mailbox/src/contract.rs @@ -1,6 +1,6 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use cosmwasm_std::{Deps, DepsMut, Env, MessageInfo, QueryResponse, Response}; +use cosmwasm_std::{Deps, DepsMut, Empty, Env, MessageInfo, QueryResponse, Response}; use hpl_interface::{ core::mailbox::{ExecuteMsg, InstantiateMsg, MailboxHookQueryMsg, MailboxQueryMsg, QueryMsg}, @@ -87,6 +87,11 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result Result { + Ok(Response::default()) +} + #[cfg(test)] mod test { use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; diff --git a/contracts/core/mailbox/src/execute.rs b/contracts/core/mailbox/src/execute.rs index 1c0bde16..2170c952 100644 --- a/contracts/core/mailbox/src/execute.rs +++ b/contracts/core/mailbox/src/execute.rs @@ -495,9 +495,12 @@ mod tests { fn test_process_query_handler(query: &WasmQuery) -> QuerierResult { match query { WasmQuery::Smart { contract_addr, msg } => { - if let Ok(req) = cosmwasm_std::from_binary::(msg) { + if let Ok(req) = cosmwasm_std::from_binary::(msg) + { match req { - ism::IsmSpecifierQueryMsg::InterchainSecurityModule() => { + hpl_interface::ism::ExpectedIsmSpecifierQueryMsg::IsmSpecifier( + ism::IsmSpecifierQueryMsg::InterchainSecurityModule(), + ) => { return SystemResult::Ok( cosmwasm_std::to_binary(&ism::InterchainSecurityModuleResponse { ism: Some(addr("default_ism")), diff --git a/contracts/core/mailbox/src/lib.rs b/contracts/core/mailbox/src/lib.rs index 87a985c6..81ac1eba 100644 --- a/contracts/core/mailbox/src/lib.rs +++ b/contracts/core/mailbox/src/lib.rs @@ -7,7 +7,7 @@ mod state; pub use crate::error::ContractError; -pub const MAILBOX_VERSION: u8 = 0; +pub const MAILBOX_VERSION: u8 = 3; // version info for migration info const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); diff --git a/contracts/core/va/src/contract.rs b/contracts/core/va/src/contract.rs index cfb4a29f..76b567af 100644 --- a/contracts/core/va/src/contract.rs +++ b/contracts/core/va/src/contract.rs @@ -1,8 +1,8 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - ensure, Addr, Deps, DepsMut, Empty, Env, Event, HexBinary, MessageInfo, Order, QueryResponse, - Response, StdResult, + ensure, ensure_eq, Deps, DepsMut, Empty, Env, Event, HexBinary, MessageInfo, Order, + QueryResponse, Response, StdResult, }; use hpl_interface::{ @@ -14,13 +14,12 @@ use hpl_interface::{ }, }, to_binary, - types::{bech32_decode, bech32_encode, eth_hash, keccak256_hash, pub_to_addr}, + types::{bech32_decode, eth_addr, eth_hash, keccak256_hash}, }; -use k256::{ecdsa::VerifyingKey, EncodedPoint}; use crate::{ error::ContractError, - state::{HRP, LOCAL_DOMAIN, MAILBOX, REPLAY_PROTECITONS, STORAGE_LOCATIONS, VALIDATORS}, + state::{LOCAL_DOMAIN, MAILBOX, REPLAY_PROTECITONS, STORAGE_LOCATIONS, VALIDATORS}, CONTRACT_NAME, CONTRACT_VERSION, }; @@ -34,6 +33,7 @@ pub fn instantiate( cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; let mailbox = deps.api.addr_validate(&msg.mailbox)?; + let mailbox_addr = bech32_decode(mailbox.as_str())?; let local_domain = deps .querier @@ -43,8 +43,7 @@ pub fn instantiate( )? .local_domain; - HRP.save(deps.storage, &msg.hrp)?; - MAILBOX.save(deps.storage, &mailbox)?; + MAILBOX.save(deps.storage, &mailbox_addr)?; LOCAL_DOMAIN.save(deps.storage, &local_domain)?; Ok(Response::new().add_event( @@ -85,18 +84,13 @@ fn get_announce( deps: Deps, validators: Vec, ) -> Result { - let hrp = HRP.load(deps.storage)?; - let storage_locations = validators .into_iter() .map(|v| { - let raw_validator = bech32_encode(&hrp, &v)?; - let validator = deps.api.addr_validate(raw_validator.as_str())?; - let storage_locations = STORAGE_LOCATIONS - .may_load(deps.storage, validator.clone())? + .may_load(deps.storage, v.to_vec())? .unwrap_or_default(); - Ok((validator.to_string(), storage_locations)) + Ok((v.to_hex(), storage_locations)) }) .collect::>>()?; @@ -106,27 +100,24 @@ fn get_announce( fn get_validators(deps: Deps) -> Result { let validators = VALIDATORS .keys(deps.storage, None, None, Order::Ascending) - .map(|v| v.map(String::from)) + .map(|v| v.map(HexBinary::from).map(|v| v.to_hex())) .collect::>>()?; Ok(GetAnnouncedValidatorsResponse { validators }) } -fn replay_hash(validator: &Addr, storage_location: &str) -> StdResult { +fn replay_hash(validator: &HexBinary, storage_location: &str) -> StdResult { Ok(keccak256_hash( - [ - bech32_decode(validator.as_str())?, - storage_location.as_bytes().to_vec(), - ] - .concat() - .as_slice(), + [validator.to_vec(), storage_location.as_bytes().to_vec()] + .concat() + .as_slice(), )) } -fn domain_hash(local_domain: u32, mailbox: &str) -> StdResult { +fn domain_hash(local_domain: u32, mailbox: HexBinary) -> StdResult { let mut bz = vec![]; bz.append(&mut local_domain.to_be_bytes().to_vec()); - bz.append(&mut bech32_decode(mailbox)?); + bz.append(&mut mailbox.to_vec()); bz.append(&mut "HYPERLANE_ANNOUNCEMENT".as_bytes().to_vec()); let hash = keccak256_hash(&bz); @@ -149,9 +140,11 @@ fn announce( storage_location: String, signature: HexBinary, ) -> Result { - let hrp = HRP.load(deps.storage)?; - let raw_validator = bech32_encode(hrp.as_str(), &validator)?; - let validator = deps.api.addr_validate(raw_validator.as_str())?; + ensure_eq!( + validator.len(), + 20, + ContractError::invalid_addr("length should be 20") + ); // check replay protection let replay_id = replay_hash(&validator, &storage_location)?; @@ -163,72 +156,79 @@ fn announce( // make announcement digest let local_domain = LOCAL_DOMAIN.load(deps.storage)?; - let mailbox = MAILBOX.load(deps.storage)?; + let mailbox_addr = MAILBOX.load(deps.storage)?; // make digest let message_hash = eth_hash(announcement_hash( - domain_hash(local_domain, mailbox.as_str())?.to_vec(), + domain_hash(local_domain, mailbox_addr.into())?.to_vec(), &storage_location, ))?; // recover pubkey from signature & verify - let recovered = deps.api.secp256k1_recover_pubkey( + let pubkey = deps.api.secp256k1_recover_pubkey( &message_hash, &signature.as_slice()[..64], // We subs 27 according to this - https://eips.ethereum.org/EIPS/eip-155 signature[64] - 27, )?; - let pubkey = EncodedPoint::from_bytes(recovered).expect("failed to parse recovered pubkey"); - let pubkey = VerifyingKey::from_encoded_point(&pubkey).expect("invalid recovered public key"); - let pubkey_bin = pubkey.to_encoded_point(true).as_bytes().to_vec(); - - let recovered_addr = bech32_encode(&hrp, &pub_to_addr(pubkey_bin.into())?)?; - ensure!(recovered_addr == validator, ContractError::VerifyFailed {}); + ensure_eq!( + eth_addr(pubkey.into())?, + validator, + ContractError::VerifyFailed {} + ); // save validator if not saved yet - if !VALIDATORS.has(deps.storage, validator.clone()) { - VALIDATORS.save(deps.storage, validator.clone(), &Empty {})?; + if !VALIDATORS.has(deps.storage, validator.to_vec()) { + VALIDATORS.save(deps.storage, validator.to_vec(), &Empty {})?; } // append storage_locations let mut storage_locations = STORAGE_LOCATIONS - .may_load(deps.storage, validator.clone())? + .may_load(deps.storage, validator.to_vec())? .unwrap_or_default(); storage_locations.push(storage_location.clone()); - STORAGE_LOCATIONS.save(deps.storage, validator.clone(), &storage_locations)?; + STORAGE_LOCATIONS.save(deps.storage, validator.to_vec(), &storage_locations)?; Ok(Response::new().add_event( Event::new("validator-announcement") .add_attribute("sender", info.sender) - .add_attribute("validator", validator) + .add_attribute("validator", validator.to_string()) .add_attribute("storage-location", storage_location), )) } +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: Empty) -> Result { + Ok(Response::new()) +} + #[cfg(test)] mod test { use cosmwasm_std::{ testing::{mock_dependencies, mock_env, mock_info}, ContractResult, QuerierResult, SystemResult, WasmQuery, }; - use ibcx_test_utils::{gen_addr, gen_bz}; + + use hpl_interface::build_test_querier; + use ibcx_test_utils::{gen_addr, gen_bz, hex}; use k256::{ ecdsa::{RecoveryId, Signature, SigningKey}, elliptic_curve::{rand_core::OsRng, sec1::ToEncodedPoint}, SecretKey, }; use rstest::rstest; - use serde::de::DeserializeOwned; use super::*; + build_test_querier!(crate::contract::query); + struct Announcement { - validator: String, + validator: HexBinary, mailbox: String, domain: u32, location: String, - signature: String, + signature: HexBinary, } impl Announcement { @@ -240,37 +240,27 @@ mod test { signature: &str, ) -> Self { Self { - validator: validator.into(), + validator: hex(validator), mailbox: mailbox.into(), domain, location: location.into(), - signature: signature.into(), + signature: hex(signature), } } - fn preset1() -> Self { - Self::new( - "f9e25a6be80f6d48727e42381fc3c3b7834c0cb4", - "62634b0c56b57fef1c27f25039cfb872875a9eeeb42d80a034f8d6b55ed20d09", - 26658, - "file:///var/folders/3v/g38z040x54x8l6b160vv66b40000gn/T/.tmp7XoxND/checkpoint", - "6c30e1072f0e23694d3a3a96dc41fc4d17636ce145e83adef3224a6f4732c2db715407b42478c581b6ac1b79e64807a7748935d398a33bf4b73d37924c293c941b", - ) - } - - fn preset2() -> Self { + fn preset() -> Self { Self::new( - "f9e25a6be80f6d48727e42381fc3c3b7834c0cb4", - "62634b0c56b57fef1c27f25039cfb872875a9eeeb42d80a034f8d6b55ed20d09", - 26657, - "file:///var/folders/3v/g38z040x54x8l6b160vv66b40000gn/T/.tmpBJPK8C/checkpoint", - "76c637d605f683734c672c0437f14ae48520e85fb68b0c0b9c28069f183e3bfc46f0de0655f06937c74b5a0a15f5b8fe37f1d1ad4dd8b64dc55307a2103fedad1c", + "05a9b5efe9f61f9142453d8e9f61565f333c6768", + "00000000000000000000000049cfd6ef774acab14814d699e3f7ee36fdfba932", + 5, + "s3://hyperlane-testnet4-goerli-validator-0/us-east-1", + "dc47d48744fdb42b983f0244ed397feac08ee556eb48416582b5b638ada7b5322c8822e56a9020de7fe663ad43070f04b341514faf430ebf880bb1932434027d1c", ) } fn rand() -> Self { // prepare test data - let mailbox = gen_bz(20); + let mailbox = gen_bz(32); let local_domain = 26657; let storage_location = "file://foo/bar"; @@ -279,18 +269,12 @@ mod test { let pubkey = secret_key.public_key(); let signing_key = SigningKey::from(secret_key); - let pubkey_bin = pubkey.to_encoded_point(true).as_bytes().to_vec(); - - let addr_bin = pub_to_addr(pubkey_bin.into()).unwrap(); + let pubkey_bin = pubkey.to_encoded_point(false).as_bytes().to_vec(); + let addr_bin = eth_addr(pubkey_bin.into()).unwrap(); // make announcement data let verify_digest = eth_hash(announcement_hash( - domain_hash( - local_domain, - bech32_encode("asdf", mailbox.as_slice()).unwrap().as_str(), - ) - .unwrap() - .to_vec(), + domain_hash(local_domain, mailbox.clone()).unwrap().to_vec(), storage_location, )) .unwrap(); @@ -301,11 +285,11 @@ mod test { ); Self { - validator: addr_bin.to_hex(), + validator: addr_bin, mailbox: mailbox.to_hex(), domain: local_domain, location: storage_location.to_string(), - signature: signature.to_hex(), + signature, } } @@ -324,11 +308,6 @@ mod test { bz.into() } - fn query(deps: Deps, msg: QueryMsg) -> T { - let res = super::query(deps, mock_env(), msg).unwrap(); - cosmwasm_std::from_binary(&res).unwrap() - } - #[rstest] fn test_init(#[values("osmo", "neutron")] hrp: &str) { let sender = gen_addr(hrp); @@ -355,26 +334,16 @@ mod test { }, ) .unwrap(); - - assert_eq!(HRP.load(deps.as_ref().storage).unwrap(), hrp); } #[rstest] - fn test_query( - #[values("osmo", "neutron")] hrp: &str, - #[values(0, 4)] validators_len: usize, - #[values(0, 4)] locations_len: usize, - ) { + fn test_queries(#[values(0, 4)] validators_len: usize, #[values(0, 4)] locations_len: usize) { let mut deps = mock_dependencies(); - HRP.save(deps.as_mut().storage, &hrp.to_string()).unwrap(); - - let validators = (0..validators_len) - .map(|_| gen_addr(hrp)) - .collect::>(); + let validators = (0..validators_len).map(|_| gen_bz(20)).collect::>(); for validator in validators { VALIDATORS - .save(deps.as_mut().storage, validator.clone(), &Empty {}) + .save(deps.as_mut().storage, validator.to_vec(), &Empty {}) .unwrap(); let locations = (0..locations_len) @@ -382,21 +351,18 @@ mod test { .collect::>(); STORAGE_LOCATIONS - .save(deps.as_mut().storage, validator, &locations) + .save(deps.as_mut().storage, validator.to_vec(), &locations) .unwrap(); } let GetAnnouncedValidatorsResponse { validators } = - query(deps.as_ref(), QueryMsg::GetAnnouncedValidators {}); + test_query(deps.as_ref(), QueryMsg::GetAnnouncedValidators {}); assert_eq!(validators.len(), validators_len); - let GetAnnounceStorageLocationsResponse { storage_locations } = query( + let GetAnnounceStorageLocationsResponse { storage_locations } = test_query( deps.as_ref(), QueryMsg::GetAnnounceStorageLocations { - validators: validators - .iter() - .map(|v| HexBinary::from(bech32_decode(v).unwrap())) - .collect::>(), + validators: validators.iter().map(|v| hex(v)).collect(), }, ); for (validator, locations) in storage_locations { @@ -407,32 +373,25 @@ mod test { #[rstest] #[case::rand(Announcement::rand(), false)] - #[case::actual_data_1(Announcement::preset1(), false)] - #[case::actual_data_2(Announcement::preset2(), false)] + #[case::actual_data_1(Announcement::preset(), false)] #[should_panic(expected = "unauthorized")] #[case::replay(Announcement::rand(), true)] #[should_panic(expected = "verify failed")] #[case::verify(Announcement::fail(), false)] - fn test_announce( - #[values("osmo", "neutron")] hrp: &str, - #[case] announcement: Announcement, - #[case] enable_duplication: bool, - ) { - let validator = HexBinary::from_hex(&announcement.validator).unwrap(); - let validator_addr = bech32_encode(hrp, validator.as_slice()).unwrap(); - + fn test_announce(#[case] announcement: Announcement, #[case] enable_duplication: bool) { + let validator = announcement.validator; let mailbox = HexBinary::from_hex(&announcement.mailbox).unwrap(); - let mailbox_addr = bech32_encode(hrp, mailbox.as_slice()).unwrap(); let mut deps = mock_dependencies(); - HRP.save(deps.as_mut().storage, &hrp.to_string()).unwrap(); LOCAL_DOMAIN .save(deps.as_mut().storage, &announcement.domain) .unwrap(); - MAILBOX.save(deps.as_mut().storage, &mailbox_addr).unwrap(); + MAILBOX + .save(deps.as_mut().storage, &mailbox.to_vec()) + .unwrap(); - let replay_id = replay_hash(&validator_addr, &announcement.location).unwrap(); + let replay_id = replay_hash(&validator, &announcement.location).unwrap(); if enable_duplication { REPLAY_PROTECITONS .save(deps.as_mut().storage, replay_id.to_vec(), &Empty {}) @@ -441,20 +400,20 @@ mod test { announce( deps.as_mut(), - mock_info(validator_addr.as_str(), &[]), - validator, + mock_info("someone", &[]), + validator.clone(), announcement.location.clone(), - HexBinary::from_hex(&announcement.signature).unwrap(), + announcement.signature, ) .map_err(|e| e.to_string()) .unwrap(); // check state assert!(REPLAY_PROTECITONS.has(deps.as_ref().storage, replay_id.to_vec())); - assert!(VALIDATORS.has(deps.as_ref().storage, validator_addr.clone())); + assert!(VALIDATORS.has(deps.as_ref().storage, validator.to_vec())); assert_eq!( STORAGE_LOCATIONS - .load(deps.as_ref().storage, validator_addr) + .load(deps.as_ref().storage, validator.to_vec()) .unwrap(), vec![announcement.location] ); diff --git a/contracts/core/va/src/error.rs b/contracts/core/va/src/error.rs index cd37059d..1cd84e7f 100644 --- a/contracts/core/va/src/error.rs +++ b/contracts/core/va/src/error.rs @@ -12,6 +12,15 @@ pub enum ContractError { #[error("unauthorized")] Unauthorized {}, + #[error("invalid address. reason: {0}")] + InvalidAddress(String), + #[error("verify failed")] VerifyFailed {}, } + +impl ContractError { + pub fn invalid_addr(reason: &str) -> Self { + ContractError::InvalidAddress(reason.into()) + } +} diff --git a/contracts/core/va/src/state.rs b/contracts/core/va/src/state.rs index a4d58a46..103fc8c7 100644 --- a/contracts/core/va/src/state.rs +++ b/contracts/core/va/src/state.rs @@ -1,20 +1,17 @@ -use cosmwasm_std::{Addr, Empty}; +use cosmwasm_std::Empty; use cw_storage_plus::{Item, Map}; -pub const HRP_KEY: &str = "hrp"; -pub const HRP: Item = Item::new(HRP_KEY); - pub const MAILBOX_KEY: &str = "mailbox"; -pub const MAILBOX: Item = Item::new(MAILBOX_KEY); +pub const MAILBOX: Item> = Item::new(MAILBOX_KEY); pub const LOCAL_DOMAIN_KEY: &str = "local_domain"; pub const LOCAL_DOMAIN: Item = Item::new(LOCAL_DOMAIN_KEY); pub const VALIDATORS_PREFIX: &str = "validators"; -pub const VALIDATORS: Map = Map::new(VALIDATORS_PREFIX); +pub const VALIDATORS: Map, Empty> = Map::new(VALIDATORS_PREFIX); pub const STORAGE_LOCATIONS_PREFIX: &str = "storage_locations"; -pub const STORAGE_LOCATIONS: Map> = Map::new(STORAGE_LOCATIONS_PREFIX); +pub const STORAGE_LOCATIONS: Map, Vec> = Map::new(STORAGE_LOCATIONS_PREFIX); pub const REPLAY_PROTECTIONS_PREFIX: &str = "replay_protections"; pub const REPLAY_PROTECITONS: Map, Empty> = Map::new(REPLAY_PROTECTIONS_PREFIX); diff --git a/contracts/hooks/aggregate/Cargo.toml b/contracts/hooks/aggregate/Cargo.toml new file mode 100644 index 00000000..7d196235 --- /dev/null +++ b/contracts/hooks/aggregate/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "hpl-hook-aggregate" +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/aggregate/src/error.rs b/contracts/hooks/aggregate/src/error.rs new file mode 100644 index 00000000..babe75a3 --- /dev/null +++ b/contracts/hooks/aggregate/src/error.rs @@ -0,0 +1,16 @@ +use cosmwasm_std::StdError; + +#[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 {}, +} diff --git a/contracts/hooks/aggregate/src/lib.rs b/contracts/hooks/aggregate/src/lib.rs new file mode 100644 index 00000000..4ffcb579 --- /dev/null +++ b/contracts/hooks/aggregate/src/lib.rs @@ -0,0 +1,186 @@ +mod error; + +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + ensure_eq, Addr, Coin, CosmosMsg, Deps, DepsMut, Env, Event, HexBinary, MessageInfo, + QueryResponse, Response, StdResult, +}; +use cw_storage_plus::Item; +use error::ContractError; +use hpl_interface::{ + hook::{ + aggregate::{AggregateHookQueryMsg, ExecuteMsg, HooksResponse, InstantiateMsg, QueryMsg}, + post_dispatch, HookQueryMsg, MailboxResponse, PostDispatchMsg, QuoteDispatchMsg, + QuoteDispatchResponse, + }, + to_binary, + types::Message, +}; +use hpl_ownable::get_owner; + +// 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 HOOKS_KEY: &str = "hooks"; +pub const HOOKS: Item> = Item::new(HOOKS_KEY); + +fn new_event(name: &str) -> Event { + Event::new(format!("hpl_hook_aggregate::{}", 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)?; + let hooks = msg + .hooks + .iter() + .map(|v| deps.api.addr_validate(v)) + .collect::>()?; + + hpl_ownable::initialize(deps.storage, &owner)?; + + HOOKS.save(deps.storage, &hooks)?; + + Ok(Response::new().add_event( + new_event("initialize") + .add_attribute("sender", info.sender) + .add_attribute("owner", owner) + .add_attribute("hooks", msg.hooks.join(",")), + )) +} + +#[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::PostDispatch(PostDispatchMsg { message, metadata }) => { + // aggregate it + let hooks = HOOKS.load(deps.storage)?; + + let msgs: Vec = hooks + .into_iter() + .map(|v| { + let quote = hpl_interface::hook::quote_dispatch( + &deps.querier, + &v, + metadata.clone(), + message.clone(), + )?; + let msg = post_dispatch( + v, + metadata.clone(), + message.clone(), + quote.gas_amount.map(|v| vec![v]), + )? + .into(); + + Ok(msg) + }) + .collect::>()?; + + let decoded_msg: Message = message.into(); + + // do nothing + Ok(Response::new().add_messages(msgs).add_event( + new_event("post_dispatch").add_attribute("message_id", decoded_msg.id().to_hex()), + )) + } + ExecuteMsg::SetHooks { hooks } => { + ensure_eq!( + get_owner(deps.storage)?, + info.sender, + ContractError::Unauthorized {} + ); + + let parsed_hooks = hooks + .iter() + .map(|v| deps.api.addr_validate(v)) + .collect::>()?; + + HOOKS.save(deps.storage, &parsed_hooks)?; + + Ok(Response::new().add_event( + new_event("set_hooks") + .add_attribute("sender", info.sender) + .add_attribute("hooks", hooks.join(",")), + )) + } + } +} + +#[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(QuoteDispatchMsg { metadata, message }) => { + to_binary(quote_dispatch(deps, metadata, message)) + } + }, + QueryMsg::AggregateHook(msg) => match msg { + AggregateHookQueryMsg::Hooks {} => to_binary(get_hooks(deps)), + }, + } +} + +fn get_mailbox(_deps: Deps) -> Result { + Ok(MailboxResponse { + mailbox: "unrestricted".to_string(), + }) +} + +fn quote_dispatch( + deps: Deps, + metadata: HexBinary, + message: HexBinary, +) -> Result { + let hooks = HOOKS.load(deps.storage)?; + + let mut total: Option = None; + + for hook in hooks { + let res = hpl_interface::hook::quote_dispatch( + &deps.querier, + hook, + metadata.clone(), + message.clone(), + )?; + + if let Some(gas_amount) = res.gas_amount { + total = match total { + Some(mut v) => { + v.amount += gas_amount.amount; + Some(v) + } + None => Some(gas_amount), + }; + } + } + + Ok(QuoteDispatchResponse { gas_amount: total }) +} + +fn get_hooks(deps: Deps) -> Result { + Ok(HooksResponse { + hooks: HOOKS + .load(deps.storage)? + .into_iter() + .map(|v| v.into()) + .collect(), + }) +} diff --git a/contracts/hooks/merkle/src/lib.rs b/contracts/hooks/merkle/src/lib.rs index 86a94cb5..765ed396 100644 --- a/contracts/hooks/merkle/src/lib.rs +++ b/contracts/hooks/merkle/src/lib.rs @@ -1,7 +1,8 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - ensure_eq, Addr, Deps, DepsMut, Env, Event, MessageInfo, QueryResponse, Response, StdError, + ensure_eq, Addr, Deps, DepsMut, Empty, Env, Event, MessageInfo, QueryResponse, Response, + StdError, }; use cw_storage_plus::Item; use hpl_interface::{ @@ -181,10 +182,19 @@ fn get_tree_checkpoint(deps: Deps) -> Result Result { + Ok(Response::new()) +} + #[cfg(test)] mod test { use super::*; diff --git a/contracts/igps/core/src/contract.rs b/contracts/igps/core/src/contract.rs index fbd4a3f7..16cfb454 100644 --- a/contracts/igps/core/src/contract.rs +++ b/contracts/igps/core/src/contract.rs @@ -1,13 +1,15 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use cosmwasm_std::{Deps, DepsMut, Env, Event, MessageInfo, QueryResponse, Response}; +use cosmwasm_std::{Deps, DepsMut, Empty, Env, Event, MessageInfo, QueryResponse, Response}; use hpl_interface::hook::HookQueryMsg; use hpl_interface::igp::core::{ExecuteMsg, IgpQueryMsg, InstantiateMsg, QueryMsg}; use hpl_interface::igp::oracle::IgpGasOracleQueryMsg; use hpl_interface::to_binary; -use crate::{ContractError, BENEFICIARY, CONTRACT_NAME, CONTRACT_VERSION, GAS_TOKEN, HRP, MAILBOX}; +use crate::{ + ContractError, BENEFICIARY, CONTRACT_NAME, CONTRACT_VERSION, DEFAULT_GAS_USAGE, GAS_TOKEN, HRP, +}; fn new_event(name: &str) -> Event { Event::new(format!("hpl_igp_core::{}", name)) @@ -23,22 +25,22 @@ pub fn instantiate( 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)?; let beneficiary = deps.api.addr_validate(&msg.beneficiary)?; hpl_ownable::initialize(deps.storage, &owner)?; BENEFICIARY.save(deps.storage, &beneficiary)?; - MAILBOX.save(deps.storage, &mailbox)?; GAS_TOKEN.save(deps.storage, &msg.gas_token)?; HRP.save(deps.storage, &msg.hrp)?; + DEFAULT_GAS_USAGE.save(deps.storage, &msg.default_gas_usage)?; Ok(Response::new().add_event( new_event("initialize") .add_attribute("sender", info.sender) .add_attribute("owner", msg.owner) - .add_attribute("beneficiary", msg.beneficiary), + .add_attribute("beneficiary", msg.beneficiary) + .add_attribute("default_gas", msg.default_gas_usage.to_string()), )) } @@ -56,6 +58,12 @@ pub fn execute( ExecuteMsg::Router(msg) => Ok(hpl_router::handle(deps, env, info, msg)?), ExecuteMsg::PostDispatch(msg) => Ok(execute::post_dispatch(deps, info, msg)?), + ExecuteMsg::SetDefaultGas { gas } => execute::set_default_gas(deps, info, gas), + ExecuteMsg::SetGasForDomain { config } => execute::set_gas_for_domain(deps, info, config), + ExecuteMsg::UnsetGasForDomain { domains } => { + execute::unset_gas_for_domain(deps, info, domains) + } + ExecuteMsg::SetBeneficiary { beneficiary } => { execute::set_beneficiary(deps, info, beneficiary) } @@ -95,7 +103,16 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result match msg { + IgpQueryMsg::DefaultGas {} => to_binary(get_default_gas(deps)), + IgpQueryMsg::GasForDomain { domains } => to_binary(get_gas_for_domain(deps, domains)), + IgpQueryMsg::ListGasForDomains { + offset, + limit, + order, + } => to_binary(list_gas_for_domains(deps, offset, limit, order)), + IgpQueryMsg::Beneficiary {} => to_binary(get_beneficiary(deps)), + IgpQueryMsg::QuoteGasPayment { dest_domain, gas_amount, @@ -103,3 +120,8 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result Result { + Ok(Response::new()) +} diff --git a/contracts/igps/core/src/event.rs b/contracts/igps/core/src/event.rs index 4a8b496a..ad981201 100644 --- a/contracts/igps/core/src/event.rs +++ b/contracts/igps/core/src/event.rs @@ -1,5 +1,37 @@ use cosmwasm_std::{Addr, Coin, Event, HexBinary, Uint128, Uint256}; +pub fn emit_set_default_gas(owner: Addr, default_gas: u128) -> Event { + Event::new("igp-core-set-default-gas") + .add_attribute("owner", owner) + .add_attribute("default-gas", default_gas.to_string()) +} + +pub fn emit_set_gas_for_domain(owner: Addr, gas_for_domain: Vec<(u32, u128)>) -> Event { + Event::new("igp-core-set-gas-for-domain") + .add_attribute("owner", owner) + .add_attribute( + "domains", + gas_for_domain + .into_iter() + .map(|v| v.0.to_string()) + .collect::>() + .join(","), + ) +} + +pub fn emit_unset_gas_for_domain(owner: Addr, domains: Vec) -> Event { + Event::new("igp-core-unset-gas-for-domain") + .add_attribute("owner", owner) + .add_attribute( + "domains", + domains + .into_iter() + .map(|v| v.to_string()) + .collect::>() + .join(","), + ) +} + pub fn emit_set_beneficiary(owner: Addr, beneficiary: String) -> Event { Event::new("igp-core-set-beneficiary") .add_attribute("owner", owner) diff --git a/contracts/igps/core/src/execute.rs b/contracts/igps/core/src/execute.rs index c8dd1c64..1578ab78 100644 --- a/contracts/igps/core/src/execute.rs +++ b/contracts/igps/core/src/execute.rs @@ -1,6 +1,11 @@ -use crate::event::{emit_claim, emit_pay_for_gas, emit_post_dispatch, emit_set_beneficiary}; +use crate::event::{ + emit_claim, emit_pay_for_gas, emit_post_dispatch, emit_set_beneficiary, emit_set_default_gas, + emit_set_gas_for_domain, emit_unset_gas_for_domain, +}; use crate::query::quote_gas_price; -use crate::{ContractError, BENEFICIARY, DEFAULT_GAS_USAGE, GAS_TOKEN, HRP, MAILBOX}; +use crate::{ + get_default_gas, ContractError, BENEFICIARY, DEFAULT_GAS_USAGE, GAS_FOR_DOMAIN, GAS_TOKEN, HRP, +}; use cosmwasm_std::{ coins, ensure, ensure_eq, BankMsg, DepsMut, Env, HexBinary, MessageInfo, Response, Uint128, @@ -14,6 +19,58 @@ use hpl_ownable::get_owner; use std::str::FromStr; +pub fn set_default_gas( + deps: DepsMut, + info: MessageInfo, + default_gas: u128, +) -> Result { + ensure_eq!( + info.sender, + get_owner(deps.storage)?, + ContractError::Unauthorized {} + ); + + DEFAULT_GAS_USAGE.save(deps.storage, &default_gas)?; + + Ok(Response::new().add_event(emit_set_default_gas(info.sender, default_gas))) +} + +pub fn set_gas_for_domain( + deps: DepsMut, + info: MessageInfo, + config: Vec<(u32, u128)>, +) -> Result { + ensure_eq!( + info.sender, + get_owner(deps.storage)?, + ContractError::Unauthorized {} + ); + + for (domain, custom_gas) in config.clone() { + GAS_FOR_DOMAIN.save(deps.storage, domain, &custom_gas)?; + } + + Ok(Response::new().add_event(emit_set_gas_for_domain(info.sender, config))) +} + +pub fn unset_gas_for_domain( + deps: DepsMut, + info: MessageInfo, + domains: Vec, +) -> Result { + ensure_eq!( + info.sender, + get_owner(deps.storage)?, + ContractError::Unauthorized {} + ); + + for domain in domains.clone() { + GAS_FOR_DOMAIN.remove(deps.storage, domain); + } + + Ok(Response::new().add_event(emit_unset_gas_for_domain(info.sender, domains))) +} + pub fn set_beneficiary( deps: DepsMut, info: MessageInfo, @@ -55,17 +112,14 @@ pub fn post_dispatch( info: MessageInfo, req: PostDispatchMsg, ) -> Result { - ensure_eq!( - info.sender, - MAILBOX.load(deps.storage)?, - ContractError::Unauthorized {} - ); - let message: Message = req.message.clone().into(); let hrp = HRP.load(deps.storage)?; let (gas_limit, refund_address) = match req.metadata.to_vec().len() < 32 { - true => (Uint256::from(DEFAULT_GAS_USAGE), message.sender_addr(&hrp)?), + true => ( + Uint256::from(get_default_gas(deps.storage, message.dest_domain)?), + message.sender_addr(&hrp)?, + ), false => { let igp_metadata: IGPMetadata = req.metadata.clone().into(); ( diff --git a/contracts/igps/core/src/lib.rs b/contracts/igps/core/src/lib.rs index d6b60c4d..cedd7a79 100644 --- a/contracts/igps/core/src/lib.rs +++ b/contracts/igps/core/src/lib.rs @@ -7,8 +7,8 @@ pub mod query; #[cfg(test)] pub mod tests; -use cosmwasm_std::Addr; -use cw_storage_plus::Item; +use cosmwasm_std::{Addr, StdResult, Storage}; +use cw_storage_plus::{Item, Map}; pub use error::ContractError; // version info for migration info @@ -17,16 +17,25 @@ pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); // constants pub const TOKEN_EXCHANGE_RATE_SCALE: u128 = 10_000_000_000; -pub const DEFAULT_GAS_USAGE: u128 = 25_000; pub const HRP_KEY: &str = "hrp"; pub const HRP: Item = Item::new(HRP_KEY); -pub const MAILBOX_KEY: &str = "mailbox"; -pub const MAILBOX: Item = Item::new(MAILBOX_KEY); - pub const GAS_TOKEN_KEY: &str = "gas_token"; pub const GAS_TOKEN: Item = Item::new(GAS_TOKEN_KEY); +pub const DEFAULT_GAS_USAGE_KEY: &str = "default_gas_usage"; +pub const DEFAULT_GAS_USAGE: Item = Item::new(DEFAULT_GAS_USAGE_KEY); + +pub const GAS_FOR_DOMAIN_PREFIX: &str = "gas_for_domain"; +pub const GAS_FOR_DOMAIN: Map = Map::new(GAS_FOR_DOMAIN_PREFIX); + pub const BENEFICAIRY_KEY: &str = "beneficiary"; pub const BENEFICIARY: Item = Item::new(BENEFICAIRY_KEY); + +pub fn get_default_gas(storage: &dyn Storage, domain: u32) -> StdResult { + let custom_gas = GAS_FOR_DOMAIN.may_load(storage, domain)?; + let default_gas = DEFAULT_GAS_USAGE.load(storage)?; + + Ok(custom_gas.unwrap_or(default_gas)) +} diff --git a/contracts/igps/core/src/query.rs b/contracts/igps/core/src/query.rs index 5eb14279..6b672013 100644 --- a/contracts/igps/core/src/query.rs +++ b/contracts/igps/core/src/query.rs @@ -1,20 +1,55 @@ use crate::error::ContractError; -use crate::{BENEFICIARY, DEFAULT_GAS_USAGE, GAS_TOKEN, MAILBOX, TOKEN_EXCHANGE_RATE_SCALE}; +use crate::{BENEFICIARY, DEFAULT_GAS_USAGE, GAS_FOR_DOMAIN, GAS_TOKEN, TOKEN_EXCHANGE_RATE_SCALE}; -use cosmwasm_std::{coin, Addr, Deps, QuerierWrapper, Storage, Uint256}; +use cosmwasm_std::{coin, Addr, Deps, QuerierWrapper, StdResult, Storage, Uint256}; use hpl_interface::hook::{MailboxResponse, QuoteDispatchMsg, QuoteDispatchResponse}; -use hpl_interface::igp::core::{BeneficiaryResponse, QuoteGasPaymentResponse}; +use hpl_interface::igp::core::{ + BeneficiaryResponse, DefaultGasResponse, GasForDomainResponse, QuoteGasPaymentResponse, +}; use hpl_interface::igp::oracle::{self, GetExchangeRateAndGasPriceResponse, IgpGasOracleQueryMsg}; use hpl_interface::types::{IGPMetadata, Message}; +use hpl_interface::Order; -pub fn get_mailbox(deps: Deps) -> Result { - let mailbox = MAILBOX.load(deps.storage)?; - +pub fn get_mailbox(_deps: Deps) -> Result { Ok(MailboxResponse { - mailbox: mailbox.into(), + mailbox: "unrestricted".to_string(), + }) +} + +pub fn get_default_gas(deps: Deps) -> Result { + let default_gas = DEFAULT_GAS_USAGE.load(deps.storage)?; + + Ok(DefaultGasResponse { gas: default_gas }) +} + +pub fn get_gas_for_domain( + deps: Deps, + domains: Vec, +) -> Result { + Ok(GasForDomainResponse { + gas: domains + .into_iter() + .map(|v| Ok((v, GAS_FOR_DOMAIN.load(deps.storage, v)?))) + .collect::>()?, }) } +pub fn list_gas_for_domains( + deps: Deps, + offset: Option, + limit: Option, + order: Option, +) -> Result { + let ((min, max), limit, order) = hpl_interface::range_option(offset, limit, order)?; + + let gas = GAS_FOR_DOMAIN + .range(deps.storage, min, max, order.into()) + .take(limit) + .collect::>>()?; + + Ok(GasForDomainResponse { gas }) +} + pub fn get_beneficiary(deps: Deps) -> Result { let beneficairy = BENEFICIARY.load(deps.storage)?; @@ -60,24 +95,30 @@ pub fn quote_dispatch( deps: Deps, req: QuoteDispatchMsg, ) -> Result { + let igp_message: Message = req.message.into(); + let gas_limit = match req.metadata.len() < 32 { - true => Uint256::from(DEFAULT_GAS_USAGE), + true => Uint256::from(crate::get_default_gas( + deps.storage, + igp_message.dest_domain, + )?), false => { let igp_metadata: IGPMetadata = req.metadata.clone().into(); igp_metadata.gas_limit } }; - let igp_message: Message = req.message.into(); - - let quote_res = quote_gas_payment(deps, igp_message.dest_domain, gas_limit); - - Ok(QuoteDispatchResponse { - gas_amount: Some(coin( - quote_res?.gas_needed.to_string().parse::()?, + let gas_amount = quote_gas_payment(deps, igp_message.dest_domain, gas_limit)?.gas_needed; + let gas_amount = if !gas_amount.is_zero() { + Some(coin( + gas_amount.to_string().parse::()?, GAS_TOKEN.load(deps.storage)?, - )), - }) + )) + } else { + None + }; + + Ok(QuoteDispatchResponse { gas_amount }) } pub fn get_exchange_rate_and_gas_price( diff --git a/contracts/igps/core/src/tests/contract.rs b/contracts/igps/core/src/tests/contract.rs index 3ac63e3e..daf61c7a 100644 --- a/contracts/igps/core/src/tests/contract.rs +++ b/contracts/igps/core/src/tests/contract.rs @@ -1,11 +1,17 @@ use cosmwasm_std::{ coin, from_binary, - testing::{mock_dependencies, mock_env}, - to_binary, Addr, BankMsg, Coin, ContractResult, HexBinary, QuerierResult, SubMsg, SystemResult, - Uint128, Uint256, WasmQuery, + testing::{mock_dependencies, mock_env, mock_info}, + to_binary, Addr, BankMsg, Coin, ContractResult, HexBinary, Order, QuerierResult, StdResult, + SubMsg, SystemResult, Uint128, Uint256, WasmQuery, }; use hpl_interface::{ - igp::{core::GasOracleConfig, oracle}, + igp::{ + core::{ + DefaultGasResponse, ExecuteMsg, GasForDomainResponse, GasOracleConfig, IgpQueryMsg, + QueryMsg, + }, + oracle, + }, types::{IGPMetadata, Message}, }; use hpl_ownable::get_owner; @@ -13,7 +19,7 @@ use hpl_router::get_routes; use ibcx_test_utils::{addr, gen_bz}; use rstest::{fixture, rstest}; -use crate::{BENEFICIARY, DEFAULT_GAS_USAGE, GAS_TOKEN, HRP, MAILBOX}; +use crate::{get_default_gas, BENEFICIARY, DEFAULT_GAS_USAGE, GAS_TOKEN, HRP}; use super::IGP; @@ -62,22 +68,14 @@ macro_rules! arg_fixture { arg_fixture!(deployer, Addr, addr("deployer")); arg_fixture!(hrp, &'static str, "test"); arg_fixture!(owner, Addr, addr("owner")); -arg_fixture!(mailbox, Addr, addr("mailbox")); arg_fixture!(gas_token, &'static str, "utest"); arg_fixture!(beneficiary, Addr, addr("beneficiary")); #[fixture] -fn igp( - deployer: Addr, - hrp: &str, - owner: Addr, - mailbox: Addr, - gas_token: &str, - beneficiary: Addr, -) -> IGP { +fn igp(deployer: Addr, hrp: &str, owner: Addr, gas_token: &str, beneficiary: Addr) -> IGP { let mut igp = IGP::new(mock_dependencies(), mock_env()); - igp.init(&deployer, hrp, &owner, &mailbox, gas_token, &beneficiary) + igp.init(&deployer, hrp, &owner, gas_token, &beneficiary) .unwrap(); igp @@ -101,7 +99,6 @@ fn test_init(igp: IGP) { assert_eq!(get_owner(storage).unwrap(), "owner"); assert_eq!(BENEFICIARY.load(storage).unwrap(), "beneficiary"); assert_eq!(GAS_TOKEN.load(storage).unwrap(), "utest"); - assert_eq!(MAILBOX.load(storage).unwrap(), "mailbox"); assert_eq!(HRP.load(storage).unwrap(), "test"); } @@ -249,8 +246,6 @@ fn test_pay_for_gas( #[case(addr("mailbox"), true, Some(300_000))] #[case(addr("mailbox"), true, None)] #[case(addr("mailbox"), false, None)] -#[should_panic(expected = "unauthorized")] -#[case(addr("owner"), true, Some(300_000))] fn test_post_dispatch( #[values("osmo", "neutron")] hrp: &str, #[with(vec![(1, "oracle/2/150".into())])] igp_routes: (IGP, Vec<(u32, String)>), @@ -310,7 +305,10 @@ fn test_post_dispatch( .parse::() .unwrap(); - assert_eq!(gas_limit.unwrap_or(DEFAULT_GAS_USAGE), gas_amount_log); + assert_eq!( + gas_limit.unwrap_or(DEFAULT_GAS_USAGE.load(igp.deps.as_mut().storage).unwrap()), + gas_amount_log + ); } #[rstest] @@ -332,3 +330,172 @@ fn test_claim(mut igp: IGP, #[case] sender: Addr, #[case] funds: Vec) { }) ) } + +#[rstest] +#[case(addr("owner"))] +#[should_panic(expected = "unauthorized")] +#[case(addr("someone"))] +fn test_set_default_gas(mut igp: IGP, #[case] sender: Addr) { + let _resp = igp + .execute( + mock_info(sender.as_str(), &[]), + ExecuteMsg::SetDefaultGas { gas: 99_999 }, + ) + .map_err(|v| v.to_string()) + .unwrap(); + + let storage = igp.deps.as_ref().storage; + + assert_eq!(crate::DEFAULT_GAS_USAGE.load(storage).unwrap(), 99_999); +} + +#[rstest] +#[case(addr("owner"))] +#[should_panic(expected = "unauthorized")] +#[case(addr("someone"))] +fn test_set_gas_for_domain(mut igp: IGP, #[case] sender: Addr) { + let config = (1u32..5u32) + .map(|i| (i, (i * 100_000) as u128)) + .collect::>(); + + let _resp = igp + .execute( + mock_info(sender.as_str(), &[]), + ExecuteMsg::SetGasForDomain { + config: config.clone(), + }, + ) + .map_err(|v| v.to_string()) + .unwrap(); + + let storage = igp.deps.as_ref().storage; + + assert_eq!( + crate::GAS_FOR_DOMAIN + .range(storage, None, None, Order::Ascending) + .collect::>>() + .unwrap(), + config + ); +} + +#[rstest] +#[case(addr("owner"))] +#[should_panic(expected = "unauthorized")] +#[case(addr("someone"))] +fn test_unset_gas_for_domain(mut igp: IGP, #[case] sender: Addr) { + let config = (1u32..5u32) + .map(|i| (i, (i * 100_000) as u128)) + .collect::>(); + + let _resp = igp + .execute( + mock_info("owner", &[]), + ExecuteMsg::SetGasForDomain { + config: config.clone(), + }, + ) + .map_err(|v| v.to_string()) + .unwrap(); + + let _resp = igp + .execute( + mock_info(sender.as_str(), &[]), + ExecuteMsg::UnsetGasForDomain { + domains: config.iter().map(|v| v.0).collect(), + }, + ) + .map_err(|v| v.to_string()) + .unwrap(); + + let storage = igp.deps.as_ref().storage; + + assert!(crate::GAS_FOR_DOMAIN.is_empty(storage)); +} + +#[rstest] +fn test_get_default_gas(mut igp: IGP) { + let _resp = igp + .execute( + mock_info("owner", &[]), + ExecuteMsg::SetGasForDomain { + config: vec![(1, 123_456)], + }, + ) + .map_err(|v| v.to_string()) + .unwrap(); + + let storage = igp.deps.as_ref().storage; + + assert_eq!(get_default_gas(storage, 1).unwrap(), 123_456); + assert_eq!(get_default_gas(storage, 2).unwrap(), 250_000); +} + +#[rstest] +fn test_gas_query(mut igp: IGP) { + let config = (1u32..100u32) + .map(|i| (i, (i * 100_000) as u128)) + .collect::>(); + + let _resp = igp + .execute( + mock_info("owner", &[]), + ExecuteMsg::SetGasForDomain { config }, + ) + .map_err(|v| v.to_string()) + .unwrap(); + + let DefaultGasResponse { gas: default_gas } = igp + .query(QueryMsg::Igp(IgpQueryMsg::DefaultGas {})) + .unwrap(); + assert_eq!(default_gas, 250_000); + + let domain_range = 1u32..4u32; + let GasForDomainResponse { + gas: gas_for_domain, + } = igp + .query(QueryMsg::Igp(IgpQueryMsg::GasForDomain { + domains: domain_range.clone().collect(), + })) + .unwrap(); + assert_eq!( + domain_range + .map(|i| (i, (i * 100_000) as u128)) + .collect::>(), + gas_for_domain + ); + + let domain_range = 1u32..11u32; + let GasForDomainResponse { + gas: gas_for_domain, + } = igp + .query(QueryMsg::Igp(IgpQueryMsg::ListGasForDomains { + offset: None, + limit: None, + order: None, + })) + .unwrap(); + assert_eq!( + domain_range + .map(|i| (i, (i * 100_000) as u128)) + .collect::>(), + gas_for_domain + ); + + let domain_range = (90u32..100u32).rev(); + let GasForDomainResponse { + gas: gas_for_domain, + } = igp + .query(QueryMsg::Igp(IgpQueryMsg::ListGasForDomains { + offset: None, + limit: None, + order: Some(hpl_interface::Order::Desc), + })) + .unwrap(); + assert_eq!( + domain_range + .map(|i| (i, (i * 100_000) as u128)) + .collect::>(), + gas_for_domain + ); +} diff --git a/contracts/igps/core/src/tests/mod.rs b/contracts/igps/core/src/tests/mod.rs index 5abdd987..29c4cd51 100644 --- a/contracts/igps/core/src/tests/mod.rs +++ b/contracts/igps/core/src/tests/mod.rs @@ -42,7 +42,6 @@ impl IGP { sender: &Addr, hrp: &str, owner: &Addr, - mailbox: &Addr, gas_token: &str, beneficiary: &Addr, ) -> Result { @@ -53,9 +52,9 @@ impl IGP { InstantiateMsg { hrp: hrp.to_string(), owner: owner.to_string(), - mailbox: mailbox.to_string(), gas_token: gas_token.to_string(), beneficiary: beneficiary.to_string(), + default_gas_usage: 250_000, }, ) } diff --git a/contracts/isms/aggregate/Cargo.toml b/contracts/isms/aggregate/Cargo.toml new file mode 100644 index 00000000..e5c9f663 --- /dev/null +++ b/contracts/isms/aggregate/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "hpl-ism-aggregate" +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 + +sha2.workspace = true +ripemd.workspace = true + +bech32.workspace = true +schemars.workspace = true + +thiserror.workspace = true + +hpl-ownable.workspace = true +hpl-interface.workspace = true + +[dev-dependencies] + +serde.workspace = true +anyhow.workspace = true diff --git a/contracts/isms/aggregate/src/error.rs b/contracts/isms/aggregate/src/error.rs new file mode 100644 index 00000000..6ceb0a7d --- /dev/null +++ b/contracts/isms/aggregate/src/error.rs @@ -0,0 +1,17 @@ +use cosmwasm_std::{StdError, VerificationError}; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + VerificationError(#[from] VerificationError), + + #[error("unauthorized")] + Unauthorized, + + #[error("route not found")] + RouteNotFound {}, +} diff --git a/contracts/isms/aggregate/src/lib.rs b/contracts/isms/aggregate/src/lib.rs new file mode 100644 index 00000000..45d4e34e --- /dev/null +++ b/contracts/isms/aggregate/src/lib.rs @@ -0,0 +1,164 @@ +mod error; + +pub use crate::error::ContractError; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + ensure_eq, Addr, Deps, DepsMut, Empty, Env, Event, HexBinary, MessageInfo, QueryResponse, + Response, StdResult, +}; +use cw2::set_contract_version; +use cw_storage_plus::Item; +use hpl_interface::{ + ism::{ + aggregate::{AggregateIsmQueryMsg, ExecuteMsg, InstantiateMsg, IsmsResponse, QueryMsg}, + IsmQueryMsg, IsmType, ModuleTypeResponse, VerifyInfoResponse, VerifyResponse, + }, + to_binary, + types::{bech32_decode, AggregateMetadata}, +}; +use hpl_ownable::get_owner; + +// version info for migration info +pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +const ISMS_KEY: &str = "isms"; +const ISMS: Item> = Item::new(ISMS_KEY); + +const THRESHOLD_KEY: &str = "threshold"; +const THRESHOLD: Item = Item::new(THRESHOLD_KEY); + +fn new_event(name: &str) -> Event { + Event::new(format!("hpl_ism_aggregate::{}", name)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + let owner = deps.api.addr_validate(&msg.owner)?; + let isms = msg + .isms + .iter() + .map(|v| deps.api.addr_validate(v)) + .collect::>()?; + + hpl_ownable::initialize(deps.storage, &owner)?; + + ISMS.save(deps.storage, &isms)?; + THRESHOLD.save(deps.storage, &msg.threshold)?; + + Ok(Response::new().add_event( + new_event("instantiate") + .add_attribute("sender", info.sender) + .add_attribute("owner", owner) + .add_attribute("isms", msg.isms.join(",")) + .add_attribute("threshold", msg.threshold.to_string()), + )) +} + +#[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::SetIsms { isms } => { + ensure_eq!( + get_owner(deps.storage)?, + info.sender, + ContractError::Unauthorized + ); + + let parsed_isms = isms + .iter() + .map(|v| deps.api.addr_validate(v)) + .collect::>()?; + + ISMS.save(deps.storage, &parsed_isms)?; + + Ok(Response::new() + .add_event(new_event("set_isms").add_attribute("isms", isms.join(",")))) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { + use IsmQueryMsg::*; + + match msg { + QueryMsg::Ownable(msg) => Ok(hpl_ownable::handle_query(deps, env, msg)?), + + QueryMsg::Ism(msg) => match msg { + ModuleType {} => to_binary({ + Ok::<_, ContractError>(ModuleTypeResponse { + typ: IsmType::Aggregation, + }) + }), + Verify { metadata, message } => to_binary(verify(deps, metadata, message)), + VerifyInfo { message } => to_binary(verify_info(deps, message)), + }, + + QueryMsg::AggregateIsm(msg) => match msg { + AggregateIsmQueryMsg::Isms {} => Ok(cosmwasm_std::to_binary(&IsmsResponse { + isms: ISMS + .load(deps.storage)? + .into_iter() + .map(|v| v.into()) + .collect(), + })?), + }, + } +} + +fn verify( + deps: Deps, + metadata: HexBinary, + message: HexBinary, +) -> Result { + let isms = ISMS.load(deps.storage)?; + + let mut threshold = THRESHOLD.load(deps.storage)?; + + for (ism, meta) in AggregateMetadata::from_hex(metadata, isms) { + let verified = hpl_interface::ism::verify(&deps.querier, ism, meta, message.clone())?; + + if verified { + threshold -= 1; + } + + if threshold == 0 { + break; + } + } + + Ok(VerifyResponse { + verified: threshold == 0, + }) +} + +fn verify_info(deps: Deps, _message: HexBinary) -> Result { + Ok(VerifyInfoResponse { + threshold: THRESHOLD.load(deps.storage)?, + validators: ISMS + .load(deps.storage)? + .into_iter() + .map(|v| Ok(bech32_decode(v.as_str())?.into())) + .collect::>()?, + }) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: Empty) -> Result { + Ok(Response::default()) +} diff --git a/contracts/isms/multisig/Cargo.toml b/contracts/isms/multisig/Cargo.toml index c9880308..2c68489d 100644 --- a/contracts/isms/multisig/Cargo.toml +++ b/contracts/isms/multisig/Cargo.toml @@ -38,5 +38,7 @@ hpl-ownable.workspace = true hpl-interface.workspace = true [dev-dependencies] +rstest.workspace = true ibcx-test-utils.workspace = true cw-multi-test.workspace = true +k256.workspace = true diff --git a/contracts/isms/multisig/src/contract.rs b/contracts/isms/multisig/src/contract.rs index c56f600d..15a60f11 100644 --- a/contracts/isms/multisig/src/contract.rs +++ b/contracts/isms/multisig/src/contract.rs @@ -1,6 +1,6 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use cosmwasm_std::{Deps, DepsMut, Env, MessageInfo, QueryResponse, Response}; +use cosmwasm_std::{Deps, DepsMut, Empty, Env, MessageInfo, QueryResponse, Response}; use cw2::set_contract_version; use hpl_interface::{ ism::{ @@ -15,7 +15,7 @@ use hpl_interface::{ use crate::{ error::ContractError, execute, - state::{HRP, THRESHOLD, VALIDATORS}, + state::{THRESHOLD, VALIDATORS}, CONTRACT_NAME, CONTRACT_VERSION, }; @@ -32,8 +32,6 @@ pub fn instantiate( hpl_ownable::initialize(deps.storage, &owner)?; - HRP.save(deps.storage, &msg.hrp)?; - Ok(Response::new().add_attribute("method", "instantiate")) } @@ -84,14 +82,15 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result(EnrolledValidatorsResponse { - validators: validators - .0 - .into_iter() - .map(|v| v.signer.to_string()) - .collect::>(), + validators, threshold, }) }), }, } } + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: Empty) -> Result { + Ok(Response::new()) +} diff --git a/contracts/isms/multisig/src/error.rs b/contracts/isms/multisig/src/error.rs index 83b12765..70db89bc 100644 --- a/contracts/isms/multisig/src/error.rs +++ b/contracts/isms/multisig/src/error.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{StdError, VerificationError}; +use cosmwasm_std::{RecoverPubkeyError, StdError, VerificationError}; use thiserror::Error; #[derive(Error, Debug)] @@ -9,27 +9,30 @@ pub enum ContractError { #[error("{0}")] VerificationError(#[from] VerificationError), - #[error("Unauthorized")] + #[error("{0}")] + RecoverPubkeyError(#[from] RecoverPubkeyError), + + #[error("unauthorized")] Unauthorized, - #[error("Wrong length")] + #[error("wrong length")] WrongLength, - #[error("Invalid pubkey")] + #[error("invalid pubkey")] InvalidPubKey, - #[error("Ownership transfer not started")] - OwnershipTransferNotStarted, - - #[error("Ownership transfer already started")] - OwnershipTransferAlreadyStarted, + #[error("invalid address. reason: {0}")] + InvalidAddress(String), - #[error("Validator pubkey mismatched")] - ValidatorPubKeyMismatched, - - #[error("Duplicate Validator")] + #[error("duplicate validator")] ValidatorDuplicate, - #[error("Validator not exists")] + #[error("validator not exists")] ValidatorNotExist, } + +impl ContractError { + pub fn invalid_addr(reason: &str) -> Self { + ContractError::InvalidAddress(reason.into()) + } +} diff --git a/contracts/isms/multisig/src/execute.rs b/contracts/isms/multisig/src/execute.rs new file mode 100644 index 00000000..d4661fbb --- /dev/null +++ b/contracts/isms/multisig/src/execute.rs @@ -0,0 +1,319 @@ +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()); + } +} diff --git a/contracts/isms/multisig/src/execute/mod.rs b/contracts/isms/multisig/src/execute/mod.rs deleted file mode 100644 index 0f99ddcc..00000000 --- a/contracts/isms/multisig/src/execute/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod threshold; -mod validator; - -pub use threshold::{set_threshold, set_thresholds}; -pub use validator::{enroll_validator, enroll_validators, unenroll_validator}; diff --git a/contracts/isms/multisig/src/execute/threshold.rs b/contracts/isms/multisig/src/execute/threshold.rs deleted file mode 100644 index 654d07c6..00000000 --- a/contracts/isms/multisig/src/execute/threshold.rs +++ /dev/null @@ -1,136 +0,0 @@ -use cosmwasm_std::{ensure_eq, DepsMut, Event, MessageInfo, Response, StdResult}; -use hpl_interface::ism::multisig::ThresholdSet; -use hpl_ownable::get_owner; - -use crate::{event::emit_set_threshold, state::THRESHOLD, 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)) -} - -#[cfg(test)] -mod test { - use cosmwasm_std::{ - testing::{mock_dependencies, mock_info}, - Addr, Storage, - }; - - 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); - } - } -} diff --git a/contracts/isms/multisig/src/execute/validator.rs b/contracts/isms/multisig/src/execute/validator.rs deleted file mode 100644 index 10876df0..00000000 --- a/contracts/isms/multisig/src/execute/validator.rs +++ /dev/null @@ -1,456 +0,0 @@ -use cosmwasm_std::{ensure_eq, DepsMut, Event, HexBinary, MessageInfo, Response}; -use hpl_interface::ism::multisig::ValidatorSet as MsgValidatorSet; -use hpl_ownable::get_owner; - -use crate::{ - event::{emit_enroll_validator, emit_unenroll_validator}, - state::{ValidatorSet, Validators, HRP, VALIDATORS}, - verify::{self}, - ContractError, -}; - -fn assert_pubkey_validate( - validator: String, - pubkey: HexBinary, - hrp: &str, -) -> Result<(), ContractError> { - let pub_to_addr = verify::pub_to_addr(pubkey, hrp)?; - - if validator != pub_to_addr { - return Err(ContractError::ValidatorPubKeyMismatched {}); - } - - Ok(()) -} - -pub fn enroll_validator( - deps: DepsMut, - info: MessageInfo, - msg: MsgValidatorSet, -) -> Result { - ensure_eq!( - info.sender, - get_owner(deps.storage)?, - ContractError::Unauthorized {} - ); - - assert_pubkey_validate( - msg.validator.clone(), - msg.validator_pubkey.clone(), - HRP.load(deps.storage)?.as_str(), - )?; - - let candidate = deps.api.addr_validate(&msg.validator)?; - let validator_state = VALIDATORS.may_load(deps.storage, msg.domain)?; - - if let Some(mut validators) = validator_state { - if validators.0.iter().any(|v| v.signer == candidate) { - return Err(ContractError::ValidatorDuplicate {}); - } - - validators.0.push(ValidatorSet { - signer: candidate, - signer_pubkey: msg.validator_pubkey, - }); - validators.0.sort_by(|a, b| a.signer.cmp(&b.signer)); - - VALIDATORS.save(deps.storage, msg.domain, &validators)?; - } else { - let validators = Validators(vec![ValidatorSet { - signer: candidate, - signer_pubkey: msg.validator_pubkey, - }]); - - VALIDATORS.save(deps.storage, msg.domain, &validators)?; - } - - Ok(Response::new().add_event(emit_enroll_validator(msg.domain, msg.validator))) -} - -pub fn enroll_validators( - deps: DepsMut, - info: MessageInfo, - validators: Vec, -) -> Result { - ensure_eq!( - info.sender, - get_owner(deps.storage)?, - ContractError::Unauthorized {} - ); - - let hrp = HRP.load(deps.storage)?; - let mut events: Vec = Vec::new(); - - for msg in validators.into_iter() { - assert_pubkey_validate(msg.validator.clone(), msg.validator_pubkey.clone(), &hrp)?; - - let candidate = deps.api.addr_validate(&msg.validator)?; - let validators_state = VALIDATORS.may_load(deps.storage, msg.domain)?; - - if let Some(mut validators) = validators_state { - if validators.0.iter().any(|v| v.signer == candidate) { - return Err(ContractError::ValidatorDuplicate {}); - } - - validators.0.push(ValidatorSet { - signer: candidate, - signer_pubkey: msg.validator_pubkey, - }); - validators.0.sort_by(|a, b| a.signer.cmp(&b.signer)); - - VALIDATORS.save(deps.storage, msg.domain, &validators)?; - events.push(emit_enroll_validator(msg.domain, msg.validator)); - } else { - let validators = Validators(vec![ValidatorSet { - signer: candidate, - signer_pubkey: msg.validator_pubkey, - }]); - - VALIDATORS.save(deps.storage, msg.domain, &validators)?; - events.push(emit_enroll_validator(msg.domain, msg.validator)); - } - } - - Ok(Response::new().add_events(events)) -} - -pub fn unenroll_validator( - deps: DepsMut, - info: MessageInfo, - domain: u32, - validator: String, -) -> Result { - ensure_eq!( - info.sender, - get_owner(deps.storage)?, - ContractError::Unauthorized {} - ); - - let unenroll_target = deps.api.addr_validate(&validator)?; - let validators = VALIDATORS - .load(deps.storage, domain) - .map_err(|_| ContractError::ValidatorNotExist {})?; - - if !validators.0.iter().any(|v| v.signer == validator) { - return Err(ContractError::ValidatorNotExist {}); - } - - let mut validator_list: Vec = validators - .0 - .into_iter() - .filter(|v| v.signer != unenroll_target) - .collect(); - - validator_list.sort_by(|a, b| a.signer.cmp(&b.signer)); - - VALIDATORS.save(deps.storage, domain, &Validators(validator_list))?; - Ok(Response::new().add_event(emit_unenroll_validator(domain, validator))) -} - -#[cfg(test)] -mod test { - use cosmwasm_std::{ - testing::{mock_dependencies, mock_info}, - Addr, Storage, - }; - - use super::*; - const ADDR1_VAULE: &str = "addr1"; - const ADDR2_VAULE: &str = "addr2"; - - const VAL_HRP: &str = "osmo"; - const VALIDATOR_ADDR: &str = "osmo1q28uzwtvvvlkz6k84gd7flu576x2l2ry9506p5"; - const VALIDATOR_PUBKEY: &str = - "033a59bbc4cb7f1e7110541e54be1ff8de6abb75fe16adaea242c52d0d7a384baf"; - - fn mock_owner(storage: &mut dyn Storage, owner: Addr) { - hpl_ownable::initialize(storage, &owner).unwrap(); - } - - #[test] - fn test_assert_pubkey_validate() { - let validator = String::from(VALIDATOR_ADDR); - let validator_pubkey = HexBinary::from_hex(VALIDATOR_PUBKEY).unwrap(); - let hrp = String::from("osmo"); - - // fail - let invalid_validator = - assert_pubkey_validate("test".to_string(), validator_pubkey.clone(), &hrp).unwrap_err(); - - assert!(matches!( - invalid_validator, - ContractError::ValidatorPubKeyMismatched {} - )); - - // success - assert_pubkey_validate(validator, validator_pubkey, &hrp).unwrap(); - } - - #[test] - fn test_enroll_validator_failure() { - let mut deps = mock_dependencies(); - let owner = Addr::unchecked(ADDR1_VAULE); - - mock_owner(deps.as_mut().storage, owner.clone()); - - HRP.save(deps.as_mut().storage, &VAL_HRP.into()).unwrap(); - - let msg = MsgValidatorSet { - domain: 1u32, - validator: "test".to_string(), - validator_pubkey: HexBinary::from_hex(VALIDATOR_PUBKEY).unwrap(), - }; - - // unauthorized - let info = mock_info(ADDR2_VAULE, &[]); - let unauthorize_resp = enroll_validator(deps.as_mut(), info, msg).unwrap_err(); - assert!(matches!(unauthorize_resp, ContractError::Unauthorized {})); - - // already exist pubkey - let valid_message = MsgValidatorSet { - domain: 1u32, - validator: VALIDATOR_ADDR.to_string(), - validator_pubkey: HexBinary::from_hex(VALIDATOR_PUBKEY).unwrap(), - }; - VALIDATORS - .save( - deps.as_mut().storage, - 1u32, - &Validators(vec![ValidatorSet { - signer: Addr::unchecked(valid_message.validator.clone()), - signer_pubkey: valid_message.validator_pubkey.clone(), - }]), - ) - .unwrap(); - - let info = mock_info(owner.as_str(), &[]); - let duplicate_pubkey = enroll_validator(deps.as_mut(), info, valid_message).unwrap_err(); - assert!(matches!( - duplicate_pubkey, - ContractError::ValidatorDuplicate {} - )) - } - - #[test] - fn test_enroll_validator_success() { - let mut deps = mock_dependencies(); - let owner = Addr::unchecked(ADDR1_VAULE); - let validator: String = VALIDATOR_ADDR.to_string(); - let domain: u32 = 1; - - HRP.save(deps.as_mut().storage, &VAL_HRP.into()).unwrap(); - - mock_owner(deps.as_mut().storage, owner.clone()); - let msg = MsgValidatorSet { - domain, - validator: validator.clone(), - validator_pubkey: HexBinary::from_hex(VALIDATOR_PUBKEY).unwrap(), - }; - - // validators not exist - let info = mock_info(ADDR1_VAULE, &[]); - let result = enroll_validator(deps.as_mut(), info, msg.clone()).unwrap(); - - assert_eq!( - result.events, - vec![emit_enroll_validator(1u32, validator.clone())] - ); - - // check it actually save - let saved_validators = VALIDATORS.load(&deps.storage, domain).unwrap(); - assert_eq!(validator, saved_validators.0[0].signer); - - // validator is exist already - VALIDATORS - .save( - deps.as_mut().storage, - 1u32, - &Validators(vec![ValidatorSet { - signer: Addr::unchecked(ADDR2_VAULE), - signer_pubkey: msg.validator_pubkey.clone(), - }]), - ) - .unwrap(); - - let info = mock_info(owner.as_str(), &[]); - let result = enroll_validator(deps.as_mut(), info, msg).unwrap(); - - assert_eq!( - result.events, - vec![emit_enroll_validator(1u32, validator.clone())] - ); - let saved_validators = VALIDATORS.load(&deps.storage, domain).unwrap(); - assert_eq!(validator, saved_validators.0.last().unwrap().signer); - } - - #[test] - fn test_enroll_validators_failure() { - let mut deps = mock_dependencies(); - let owner = Addr::unchecked(ADDR1_VAULE); - - mock_owner(deps.as_mut().storage, owner); - - HRP.save(deps.as_mut().storage, &VAL_HRP.into()).unwrap(); - - let msg = vec![ - MsgValidatorSet { - domain: 1u32, - validator: String::from(VALIDATOR_ADDR), - validator_pubkey: HexBinary::from_hex(VALIDATOR_PUBKEY).unwrap(), - }, - MsgValidatorSet { - domain: 1u32, - validator: String::from(VALIDATOR_ADDR), - validator_pubkey: HexBinary::from_hex(VALIDATOR_PUBKEY).unwrap(), - }, - ]; - - let info = mock_info(ADDR2_VAULE, &[]); - let unauthorized = enroll_validators(deps.as_mut(), info, msg.clone()).unwrap_err(); - assert!(matches!(unauthorized, ContractError::Unauthorized {})); - - let info = mock_info(ADDR1_VAULE, &[]); - let duplicated = enroll_validators(deps.as_mut(), info, msg).unwrap_err(); - assert!(matches!(duplicated, ContractError::ValidatorDuplicate {})); - } - - #[test] - fn test_enroll_validators_success() { - let mut deps = mock_dependencies(); - let owner = Addr::unchecked(ADDR1_VAULE); - let validator = String::from(VALIDATOR_ADDR); - let validator_pubkey = HexBinary::from_hex(VALIDATOR_PUBKEY).unwrap(); - mock_owner(deps.as_mut().storage, owner.clone()); - - HRP.save(deps.as_mut().storage, &VAL_HRP.into()).unwrap(); - - let msg = vec![ - MsgValidatorSet { - domain: 1u32, - validator: validator.clone(), - validator_pubkey: validator_pubkey.clone(), - }, - MsgValidatorSet { - domain: 2u32, - validator: validator.clone(), - validator_pubkey: validator_pubkey.clone(), - }, - ]; - - VALIDATORS - .save( - deps.as_mut().storage, - 2u32, - &Validators(vec![ValidatorSet { - signer: Addr::unchecked(ADDR2_VAULE), - signer_pubkey: validator_pubkey, - }]), - ) - .unwrap(); - - let info = mock_info(owner.as_str(), &[]); - let result = enroll_validators(deps.as_mut(), info, msg).unwrap(); - - assert_eq!( - result.events, - vec![ - emit_enroll_validator(1u32, validator.clone()), - emit_enroll_validator(2u32, validator.clone()) - ] - ); - - // check it actually saved - assert_eq!( - validator, - VALIDATORS - .load(&deps.storage, 1u32) - .unwrap() - .0 - .last() - .unwrap() - .signer - ); - assert_eq!( - validator, - VALIDATORS - .load(&deps.storage, 2u32) - .unwrap() - .0 - .last() - .unwrap() - .signer - ); - } - - #[test] - fn test_unenroll_validator_failure() { - let mut deps = mock_dependencies(); - let owner = Addr::unchecked(ADDR1_VAULE); - let validator = String::from(VALIDATOR_ADDR); - let domain: u32 = 1; - - mock_owner(deps.as_mut().storage, owner.clone()); - - // unauthorization - let info = mock_info(ADDR2_VAULE, &[]); - let unauthorized = - unenroll_validator(deps.as_mut(), info, domain, validator.clone()).unwrap_err(); - assert!(matches!(unauthorized, ContractError::Unauthorized {})); - - // not exists - let info = mock_info(owner.as_str(), &[]); - let not_exist_state = - unenroll_validator(deps.as_mut(), info.clone(), domain, validator.clone()).unwrap_err(); - assert!(matches!( - not_exist_state, - ContractError::ValidatorNotExist {} - )); - - // not exists in exist state - VALIDATORS - .save( - deps.as_mut().storage, - 1u32, - &Validators(vec![ValidatorSet { - signer: Addr::unchecked(ADDR2_VAULE), - signer_pubkey: HexBinary::from_hex(VALIDATOR_PUBKEY).unwrap(), - }]), - ) - .unwrap(); - let not_exist_state = - unenroll_validator(deps.as_mut(), info, domain, validator).unwrap_err(); - assert!(matches!( - not_exist_state, - ContractError::ValidatorNotExist {} - )); - } - - #[test] - fn test_unenroll_validator_success() { - let mut deps = mock_dependencies(); - let owner = Addr::unchecked(ADDR1_VAULE); - let validator = String::from(VALIDATOR_ADDR); - let domain: u32 = 1; - - mock_owner(deps.as_mut().storage, owner.clone()); - - let info = mock_info(owner.as_str(), &[]); - VALIDATORS - .save( - deps.as_mut().storage, - domain, - &Validators(vec![ValidatorSet { - signer: Addr::unchecked(validator.clone()), - signer_pubkey: HexBinary::from_hex(VALIDATOR_PUBKEY).unwrap(), - }]), - ) - .unwrap(); - let result = unenroll_validator(deps.as_mut(), info, domain, validator.clone()).unwrap(); - - assert_eq!( - result.events, - vec![emit_unenroll_validator(domain, validator)] - ); - assert_eq!(VALIDATORS.load(&deps.storage, domain).unwrap().0.len(), 0) - } -} diff --git a/contracts/isms/multisig/src/lib.rs b/contracts/isms/multisig/src/lib.rs index 34f484b8..aba3b52f 100644 --- a/contracts/isms/multisig/src/lib.rs +++ b/contracts/isms/multisig/src/lib.rs @@ -4,7 +4,6 @@ pub mod event; pub mod execute; pub mod query; pub mod state; -mod verify; use cosmwasm_std::{HexBinary, StdResult}; use hpl_interface::types::keccak256_hash; @@ -15,8 +14,6 @@ pub use crate::error::ContractError; pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -const PREFIX: &str = "\x19Ethereum Signed Message:\n"; - pub fn domain_hash(local_domain: u32, address: HexBinary) -> StdResult { let mut bz = vec![]; bz.append(&mut local_domain.to_be_bytes().to_vec()); @@ -28,25 +25,17 @@ pub fn domain_hash(local_domain: u32, address: HexBinary) -> StdResult Result { - let mut eth_message = format!("{PREFIX}{}", message.len()).into_bytes(); - eth_message.extend_from_slice(&message); - let message_hash = keccak256_hash(ð_message); - - Ok(message_hash) -} - pub fn multisig_hash( mut domain_hash: Vec, mut root: Vec, - mut index: Vec, + index: u32, mut message_id: Vec, ) -> Result { let mut bz = vec![]; bz.append(&mut domain_hash); bz.append(&mut root); - bz.append(&mut index); + bz.append(&mut index.to_be_bytes().to_vec()); bz.append(&mut message_id); let hash = keccak256_hash(&bz); diff --git a/contracts/isms/multisig/src/query.rs b/contracts/isms/multisig/src/query.rs index b060a925..ac848b91 100644 --- a/contracts/isms/multisig/src/query.rs +++ b/contracts/isms/multisig/src/query.rs @@ -1,21 +1,15 @@ use cosmwasm_std::{Deps, HexBinary}; use hpl_interface::{ ism::{IsmType, ModuleTypeResponse, VerifyInfoResponse, VerifyResponse}, - types::{Message, MessageIdMultisigIsmMetadata}, + types::{eth_addr, eth_hash, Message, MessageIdMultisigIsmMetadata}, }; use crate::{ - domain_hash, eth_hash, multisig_hash, + domain_hash, multisig_hash, state::{THRESHOLD, VALIDATORS}, ContractError, }; -fn product(x: Vec, y: Vec) -> Vec<(T, U)> { - x.iter() - .flat_map(|item_x| y.iter().map(move |item_y| (item_x.clone(), item_y.clone()))) - .collect() -} - pub fn get_module_type() -> Result { Ok(ModuleTypeResponse { typ: IsmType::MessageIdMultisig, @@ -35,34 +29,39 @@ pub fn verify_message( let metadata: MessageIdMultisigIsmMetadata = raw_metadata.into(); let message: Message = raw_message.into(); - let threshold = THRESHOLD.load(deps.storage, message.origin_domain)?; - let validators = VALIDATORS.load(deps.storage, message.origin_domain)?; - - let verifiable_cases = product( - validators.0.into_iter().map(|v| v.signer_pubkey).collect(), - metadata.signatures, - ); + let merkle_index = metadata.merkle_index(); let multisig_hash = multisig_hash( domain_hash(message.origin_domain, metadata.origin_merkle_tree)?.to_vec(), metadata.merkle_root.to_vec(), - metadata.merkle_index.to_vec(), + merkle_index, message.id().to_vec(), )?; let hashed_message = eth_hash(multisig_hash)?; - let success: u8 = verifiable_cases - .into_iter() - .map(|v| { - deps.api - .secp256k1_verify(&hashed_message, &v.1[0..64], &v.0) - .unwrap() as u8 - }) - .sum(); + // pizza :) + let validators = VALIDATORS.load(deps.storage, message.origin_domain)?; + let mut threshold = THRESHOLD.load(deps.storage, message.origin_domain)?; + + for signature in metadata.signatures { + let signature = signature.as_slice(); + let pubkey = deps.api.secp256k1_recover_pubkey( + &hashed_message, + &signature[..64], + signature[64] - 27, + )?; + + if validators.contains(ð_addr(pubkey.into())?) { + threshold -= 1; + if threshold == 0 { + break; + } + } + } Ok(VerifyResponse { - verified: success >= threshold, + verified: threshold == 0, }) } @@ -77,28 +76,23 @@ pub fn get_verify_info( Ok(VerifyInfoResponse { threshold, - validators: validators - .0 - .into_iter() - .map(|v| v.signer.to_string()) - .collect(), + validators, }) } #[cfg(test)] mod test { - use crate::{ - query::get_verify_info, - state::{ValidatorSet, Validators, THRESHOLD, VALIDATORS}, - }; - use cosmwasm_std::testing::mock_dependencies; + use crate::state::{THRESHOLD, VALIDATORS}; + use cosmwasm_std::{testing::mock_dependencies, HexBinary}; use hpl_interface::{ - ism::{IsmType, ModuleTypeResponse, VerifyInfoResponse, VerifyResponse}, - types::{bech32_encode, Message}, + ism::{IsmType, ModuleTypeResponse, VerifyResponse}, + types::{eth_addr, Message}, }; - use ibcx_test_utils::{addr, hex}; + use ibcx_test_utils::hex; + use k256::{ecdsa::SigningKey, elliptic_curve::rand_core::OsRng}; + use rstest::rstest; - use super::{get_module_type, verify_message}; + use super::{get_module_type, get_verify_info, verify_message}; #[test] fn test_get_module_type() { @@ -112,29 +106,35 @@ mod test { ); } - #[test] - fn test_verify_with_e2e_data() { - let raw_message = hex("0000000000000068220000000000000000000000000d1255b09d94659bb0888e0aa9fca60245ce402a0000682155208cd518cffaac1b5d8df216a9bd050c9a03f0d4f3ba88e5268ac4cd12ee2d68656c6c6f"); - let raw_metadata = hex("986a1625d44e4b3969b08a5876171b2b4fcdf61b3e5c70a86ad17b304f17740a9f45d99ea6bec61392a47684f4e5d1416ddbcb5fdef0f132c27d7034e9bbff1c00000000ba9911d78ec6d561413e3589f920388cbd7554fbddd8ce50739337250853ec3577a51fa40e727c05b50f15db13f5aad5857c89d432644be48d70325ea83fdb6c1c"); - - let signer = hex("f9e25a6be80f6d48727e42381fc3c3b7834c0cb4"); - let signer = bech32_encode("osmo", signer.as_slice()).unwrap(); - let signer_pubkey = - hex("039cdc58e622e25767cfa565802534b2f777107a3382f46a88323471bbd3d84c22"); - + #[rstest] + #[case( + hex("0000000000000068220000000000000000000000000d1255b09d94659bb0888e0aa9fca60245ce402a0000682155208cd518cffaac1b5d8df216a9bd050c9a03f0d4f3ba88e5268ac4cd12ee2d68656c6c6f"), + hex("986a1625d44e4b3969b08a5876171b2b4fcdf61b3e5c70a86ad17b304f17740a9f45d99ea6bec61392a47684f4e5d1416ddbcb5fdef0f132c27d7034e9bbff1c00000000ba9911d78ec6d561413e3589f920388cbd7554fbddd8ce50739337250853ec3577a51fa40e727c05b50f15db13f5aad5857c89d432644be48d70325ea83fdb6c1c"), + vec![ + hex("122e0663ccc190266427e7fc0ed6589b5d7d36db"), + hex("01d7525e91dfc3f594fd366aad70f956b398de9e"), + ] + )] + #[case( + hex("03000000240001388100000000000000000000000004980c17e2ce26578c82f81207e706e4505fae3b0000a8690000000000000000000000000b1c1b54f45e02552331d3106e71f5e0b573d5d448656c6c6f21"), + hex("0000000000000000000000009af85731edd41e2e50f81ef8a0a69d2fb836edf9a84430f822e0e9b5942faace72bd5b97f0b59a58a9b8281231d9e5c393b5859c00000024539feceace17782697e29e74151006dc7b47227cf48aba02926336cb5f7fa38b3d05e8293045f7b5811eda3ae8aa070116bb5fbf57c79e143a69e909df90cefa1b6e6ead7180e0415c36642ee4bc5454bc4f5ca250ca77a1a83562035544e0e898734d6541a20404e05fd53eb1c75b0bd21851c3bd8122cfa3550d7b6fb94d7cee1b"), + vec![ + hex("ebc301013b6cd2548e347c28d2dc43ec20c068f2"), + hex("315db9868fc8813b221b1694f8760ece39f45447"), + hex("17517c98358c5937c5d9ee47ce1f5b4c2b7fc9f5"), + ] + )] + fn test_verify_with_e2e_data( + #[case] raw_message: HexBinary, + #[case] raw_metadata: HexBinary, + #[case] validators: Vec, + ) { let mut deps = mock_dependencies(); let message: Message = raw_message.clone().into(); VALIDATORS - .save( - deps.as_mut().storage, - message.origin_domain, - &Validators(vec![ValidatorSet { - signer, - signer_pubkey, - }]), - ) + .save(deps.as_mut().storage, message.origin_domain, &validators) .unwrap(); THRESHOLD .save(deps.as_mut().storage, message.origin_domain, &1u8) @@ -146,60 +146,23 @@ mod test { #[test] fn test_get_verify_info() { + let raw_message = hex("0000000000000068220000000000000000000000000d1255b09d94659bb0888e0aa9fca60245ce402a0000682155208cd518cffaac1b5d8df216a9bd050c9a03f0d4f3ba88e5268ac4cd12ee2d68656c6c6f"); + let mut deps = mock_dependencies(); - let message = Message { - version: 0, - nonce: 8528, - origin_domain: 44787, - sender: hex("000000000000000000000000477d860f8f41bc69ddd32821f2bf2c2af0243f16"), - dest_domain: 11155111, - recipient: hex("0000000000000000000000005d56b8a669f50193b54319442c6eee5edd662381"), - body: hex("48656c6c6f21"), - }; + let signing_key = SigningKey::random(&mut OsRng); + let verifying_key = signing_key.verifying_key(); + + let addr = eth_addr(verifying_key.to_encoded_point(false).as_bytes().into()).unwrap(); VALIDATORS - .save( - deps.as_mut().storage, - message.origin_domain, - &Validators(vec![ - ValidatorSet { - signer: addr("osmo1pql3lj3kftaf5pn507y74xfxlew0tufs8tey2k"), - signer_pubkey: hex( - "02b539cc3dbc1a266ee87648a2764ff6d85ab95384459ef2e5d4787c88724f581c", - ), - }, - ValidatorSet { - signer: addr("osmo13t2lcawapgppddj9hf0qk5yrrcvrre5gkslkat"), - signer_pubkey: hex( - "02fee848be8b8ea38d0ebae9fb11a8cdbea93f20c4accf40fde4f87ff213d09821", - ), - }, - ValidatorSet { - signer: addr("osmo1wjfete3kxrhyzcuhdp3lc6g3a8r275dp80w9xd"), - signer_pubkey: hex( - "0278360f516ef0f90c175fce5de37cc721f122780efa956f17ee08bbfcbe5cb101", - ), - }, - ]), - ) + .save(deps.as_mut().storage, 26658, &vec![addr.clone()]) .unwrap(); + THRESHOLD.save(deps.as_mut().storage, 26658, &1u8).unwrap(); - THRESHOLD - .save(deps.as_mut().storage, message.origin_domain, &2u8) - .unwrap(); + let info = get_verify_info(deps.as_ref(), raw_message).unwrap(); - let success_result = get_verify_info(deps.as_ref(), message.into()).unwrap(); - assert_eq!( - success_result, - VerifyInfoResponse { - threshold: 2, - validators: vec![ - "osmo1pql3lj3kftaf5pn507y74xfxlew0tufs8tey2k".to_string(), - "osmo13t2lcawapgppddj9hf0qk5yrrcvrre5gkslkat".to_string(), - "osmo1wjfete3kxrhyzcuhdp3lc6g3a8r275dp80w9xd".to_string(), - ] - } - ); + assert_eq!(info.validators, vec![addr]); + assert_eq!(info.threshold, 1); } } diff --git a/contracts/isms/multisig/src/state.rs b/contracts/isms/multisig/src/state.rs index 41fc124d..25c8564b 100644 --- a/contracts/isms/multisig/src/state.rs +++ b/contracts/isms/multisig/src/state.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, HexBinary}; -use cw_storage_plus::{Item, Map}; +use cw_storage_plus::Map; #[cw_serde] pub struct Config { @@ -8,20 +8,8 @@ pub struct Config { pub addr_prefix: String, } -#[cw_serde] -pub struct ValidatorSet { - pub signer: Addr, - pub signer_pubkey: HexBinary, -} - -#[cw_serde] -pub struct Validators(pub Vec); - -pub const HRP_KEY: &str = "hrp"; -pub const HRP: Item = Item::new(HRP_KEY); - pub const VALIDATORS_PREFIX: &str = "validators"; -pub const VALIDATORS: Map = Map::new(VALIDATORS_PREFIX); +pub const VALIDATORS: Map> = Map::new(VALIDATORS_PREFIX); pub const THRESHOLD_PREFIX: &str = "threshold"; pub const THRESHOLD: Map = Map::new(THRESHOLD_PREFIX); diff --git a/contracts/isms/multisig/src/verify.rs b/contracts/isms/multisig/src/verify.rs deleted file mode 100644 index 4ca11cbc..00000000 --- a/contracts/isms/multisig/src/verify.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::error::ContractError; -use bech32::ToBase32; -use cosmwasm_std::HexBinary; -use ripemd::{Digest, Ripemd160}; -use sha2::Sha256; - -pub fn sha256_digest(bz: impl AsRef<[u8]>) -> Result<[u8; 32], ContractError> { - let mut hasher = Sha256::new(); - - hasher.update(bz); - - hasher - .finalize() - .as_slice() - .try_into() - .map_err(|_| ContractError::WrongLength {}) -} - -pub fn ripemd160_digest(bz: impl AsRef<[u8]>) -> Result<[u8; 20], ContractError> { - let mut hasher = Ripemd160::new(); - - hasher.update(bz); - - hasher - .finalize() - .as_slice() - .try_into() - .map_err(|_| ContractError::WrongLength {}) -} - -pub fn pub_to_addr(pub_key: HexBinary, prefix: &str) -> Result { - let sha_hash = sha256_digest(pub_key)?; - let rip_hash = ripemd160_digest(sha_hash)?; - - let addr = bech32::encode(prefix, rip_hash.to_base32(), bech32::Variant::Bech32) - .map_err(|_| ContractError::InvalidPubKey {})?; - - Ok(addr) -} diff --git a/contracts/mocks/mock-ism/src/contract.rs b/contracts/mocks/mock-ism/src/contract.rs index 824e6901..cf673bae 100644 --- a/contracts/mocks/mock-ism/src/contract.rs +++ b/contracts/mocks/mock-ism/src/contract.rs @@ -59,7 +59,7 @@ pub fn query(_deps: Deps, _env: Env, msg: ExpectedIsmQueryMsg) -> StdResult Ok(to_binary(&VerifyResponse { verified: true })?), VerifyInfo { .. } => Ok(to_binary(&VerifyInfoResponse { threshold: 1u8, - validators: vec!["".to_string()], + validators: vec![], })?), }, } diff --git a/contracts/mocks/mock-msg-receiver/src/contract.rs b/contracts/mocks/mock-msg-receiver/src/contract.rs index ad9bb9a8..b127e108 100644 --- a/contracts/mocks/mock-msg-receiver/src/contract.rs +++ b/contracts/mocks/mock-msg-receiver/src/contract.rs @@ -2,7 +2,8 @@ use cosmwasm_schema::cw_serde; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - attr, to_binary, Deps, DepsMut, Env, Event, MessageInfo, QueryResponse, Response, StdResult, + attr, to_binary, Deps, DepsMut, Empty, Env, Event, MessageInfo, QueryResponse, Response, + StdResult, }; use cw2::set_contract_version; use cw_storage_plus::Item; @@ -15,9 +16,6 @@ pub struct InstantiateMsg { pub hrp: String, } -#[cw_serde] -pub struct MigrateMsg {} - #[cw_serde] pub struct ExecuteMsg {} @@ -38,11 +36,6 @@ pub fn instantiate( Ok(Response::new().add_attribute("method", "instantiate")) } -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult { - Ok(Response::default()) -} - /// Handling contract execution #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( @@ -67,12 +60,21 @@ pub fn execute( /// Handling contract query #[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(_deps: Deps, _env: Env, msg: ism::IsmSpecifierQueryMsg) -> StdResult { +pub fn query( + _deps: Deps, + _env: Env, + msg: ism::ExpectedIsmSpecifierQueryMsg, +) -> StdResult { match msg { - ism::IsmSpecifierQueryMsg::InterchainSecurityModule() => { - Ok(to_binary(&ism::InterchainSecurityModuleResponse { - ism: None, - })?) - } + ism::ExpectedIsmSpecifierQueryMsg::IsmSpecifier( + ism::IsmSpecifierQueryMsg::InterchainSecurityModule(), + ) => Ok(to_binary(&ism::InterchainSecurityModuleResponse { + ism: None, + })?), } } + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: Empty) -> StdResult { + Ok(Response::default()) +} diff --git a/contracts/warp/cw20/Cargo.toml b/contracts/warp/cw20/Cargo.toml index 593cc1ba..10cbe744 100644 --- a/contracts/warp/cw20/Cargo.toml +++ b/contracts/warp/cw20/Cargo.toml @@ -38,11 +38,16 @@ schemars.workspace = true thiserror.workspace = true +hpl-connection.workspace = true hpl-ownable.workspace = true hpl-router.workspace = true hpl-interface.workspace = true [dev-dependencies] +serde-json-wasm.workspace = true + +osmosis-test-tube.workspace = true +ibcx-test-utils.workspace = true rstest.workspace = true anyhow.workspace = true k256.workspace = true diff --git a/contracts/warp/cw20/src/contract.rs b/contracts/warp/cw20/src/contract.rs index aade209e..defe13b2 100644 --- a/contracts/warp/cw20/src/contract.rs +++ b/contracts/warp/cw20/src/contract.rs @@ -1,18 +1,20 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - ensure_eq, from_binary, CosmosMsg, Deps, DepsMut, Env, Event, HexBinary, MessageInfo, - QueryResponse, Reply, Response, SubMsg, Uint256, WasmMsg, + ensure_eq, wasm_execute, CosmosMsg, Deps, DepsMut, Env, HexBinary, MessageInfo, QueryResponse, + Reply, Response, SubMsg, Uint128, Uint256, WasmMsg, }; -use cw20::Cw20ReceiveMsg; +use cw20::Cw20ExecuteMsg; +use hpl_connection::{get_hook, get_ism}; use hpl_interface::{ core::mailbox, + ism::{InterchainSecurityModuleResponse, IsmSpecifierQueryMsg}, to_binary, types::bech32_encode, warp::{ self, - cw20::{ExecuteMsg, InstantiateMsg, QueryMsg, ReceiveMsg}, + cw20::{ExecuteMsg, InstantiateMsg, QueryMsg}, TokenMode, TokenModeMsg, TokenModeResponse, TokenTypeResponse, }, }; @@ -34,33 +36,39 @@ pub fn instantiate( let mode: TokenMode = msg.token.clone().into(); let owner = deps.api.addr_validate(&msg.owner)?; + let mailbox = deps.api.addr_validate(&msg.mailbox)?; HRP.save(deps.storage, &msg.hrp)?; MODE.save(deps.storage, &mode)?; - MAILBOX.save(deps.storage, &deps.api.addr_validate(&msg.mailbox)?)?; + MAILBOX.save(deps.storage, &mailbox)?; hpl_ownable::initialize(deps.storage, &owner)?; - let mut denom = "".into(); - - let msgs = match msg.token { + let (msgs, denom) = match msg.token { TokenModeMsg::Bridged(token) => { - vec![SubMsg::reply_on_success( + let mut token_init_msg = token.init_msg; + token_init_msg.mint = Some(cw20::MinterResponse { + minter: env.contract.address.to_string(), + cap: None, + }); + + let msgs = vec![SubMsg::reply_on_success( WasmMsg::Instantiate { admin: Some(env.contract.address.to_string()), code_id: token.code_id, - msg: cosmwasm_std::to_binary(&token.init_msg)?, + msg: cosmwasm_std::to_binary(&token_init_msg)?, funds: vec![], label: "token warp cw20".to_string(), }, REPLY_ID_CREATE_DENOM, - )] + )]; + + (msgs, token_init_msg.name) } TokenModeMsg::Collateral(token) => { let token_addr = deps.api.addr_validate(&token.address)?; TOKEN.save(deps.storage, &token_addr)?; - denom = token_addr.to_string(); - vec![] + (vec![], token_addr.into()) } }; @@ -83,22 +91,15 @@ pub fn execute( use ExecuteMsg::*; match msg { + Ownable(msg) => Ok(hpl_ownable::handle(deps, env, info, msg)?), Router(msg) => Ok(hpl_router::handle(deps, env, info, msg)?), + Connection(msg) => Ok(hpl_connection::handle(deps, env, info, msg)?), Handle(msg) => mailbox_handle(deps, info, msg), - Receive(msg) => { - ensure_eq!( - info.sender, - TOKEN.load(deps.storage)?, - ContractError::Unauthorized - ); - - match from_binary::(&msg.msg)? { - ReceiveMsg::TransferRemote { - dest_domain, - recipient, - } => transfer_remote(deps, msg, dest_domain, recipient), - } - } + TransferRemote { + dest_domain, + recipient, + amount, + } => transfer_remote(deps, env, info, dest_domain, recipient, amount), } } @@ -112,9 +113,8 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result Result { let token = TOKEN.load(deps.storage)?; let mode = MODE.load(deps.storage)?; @@ -181,34 +183,48 @@ fn transfer_remote( let mut msgs: Vec = vec![]; + // push token transfer msg + msgs.push( + wasm_execute( + &token, + &Cw20ExecuteMsg::TransferFrom { + owner: info.sender.to_string(), + recipient: env.contract.address.to_string(), + amount: transfer_amount, + }, + vec![], + )? + .into(), + ); + if mode == TokenMode::Bridged { // push token burn msg if token is bridged - msgs.push(conv::to_burn_msg(&token, receive_msg.amount)?.into()); + msgs.push(conv::to_burn_msg(&token, transfer_amount)?.into()); } - let dispatch_payload = warp::Message { - recipient: recipient.clone(), - amount: Uint256::from_uint128(receive_msg.amount), - metadata: HexBinary::default(), - }; - // push mailbox dispatch msg msgs.push(mailbox::dispatch( mailbox, dest_domain, dest_router, - dispatch_payload.into(), - None, + warp::Message { + recipient: recipient.clone(), + amount: Uint256::from_uint128(transfer_amount), + metadata: HexBinary::default(), + } + .into(), + get_hook(deps.storage)?.map(|v| v.into()), None, + info.funds, )?); Ok(Response::new().add_messages(msgs).add_event( new_event("transfer-remote") - .add_attribute("sender", receive_msg.sender) + .add_attribute("sender", info.sender) .add_attribute("dest_domain", dest_domain.to_string()) .add_attribute("recipient", recipient.to_hex()) .add_attribute("token", token) - .add_attribute("amount", receive_msg.amount), + .add_attribute("amount", transfer_amount), )) } @@ -219,10 +235,16 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result Ok(hpl_ownable::handle_query(deps, env, msg)?), QueryMsg::Router(msg) => Ok(hpl_router::handle_query(deps, env, msg)?), + QueryMsg::Connection(msg) => Ok(hpl_connection::handle_query(deps, env, msg)?), QueryMsg::TokenDefault(msg) => match msg { TokenType {} => to_binary(get_token_type(deps)), TokenMode {} => to_binary(get_token_mode(deps)), }, + QueryMsg::IsmSpecifier(IsmSpecifierQueryMsg::InterchainSecurityModule()) => Ok( + cosmwasm_std::to_binary(&InterchainSecurityModuleResponse { + ism: get_ism(deps.storage)?, + })?, + ), } } @@ -239,3 +261,319 @@ fn get_token_mode(deps: Deps) -> Result { Ok(TokenModeResponse { mode }) } + +#[cfg(test)] +mod test { + use cosmwasm_std::{ + testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage}, + Empty, OwnedDeps, Uint128, + }; + use hpl_interface::{ + build_test_executor, build_test_querier, + core::HandleMsg, + router::DomainRouteSet, + warp::cw20::{Cw20ModeBridged, Cw20ModeCollateral}, + }; + use hpl_router::set_routes; + use ibcx_test_utils::{addr, gen_bz}; + use rstest::{fixture, rstest}; + + use super::*; + + build_test_querier!(super::query); + build_test_executor!(super::execute); + + const DEPLOYER: &str = "sender"; + const OWNER: &str = "owner"; + const MAILBOX: &str = "mailbox"; + const TOKEN: &str = "token"; + + const CW20_BRIDGED_CODE_ID: u64 = 1; + const CW20_BRIDGED_NAME: &str = "cw20-created"; + const CW20_COLLATERAL_ADDRESS: &str = "cw20-exisiting"; + + type Cw20TokenMode = TokenModeMsg; + type TestDeps = OwnedDeps; + + #[fixture] + fn token_mode_bridged() -> Cw20TokenMode { + TokenModeMsg::Bridged(Cw20ModeBridged { + code_id: CW20_BRIDGED_CODE_ID, + init_msg: cw20_base::msg::InstantiateMsg { + name: CW20_BRIDGED_NAME.to_string(), + symbol: CW20_BRIDGED_NAME.to_string(), + decimals: 1, + initial_balances: vec![], + mint: None, + marketing: None, + } + .into(), + }) + } + + #[fixture] + fn token_mode_collateral() -> Cw20TokenMode { + TokenModeMsg::Collateral(Cw20ModeCollateral { + address: CW20_COLLATERAL_ADDRESS.to_string(), + }) + } + + #[fixture] + fn deps( + #[default(vec![])] routes: Vec<(u32, HexBinary)>, + #[default("osmo")] hrp: &str, + #[default(Some(TOKEN))] token: Option<&str>, + token_mode_collateral: Cw20TokenMode, + ) -> (TestDeps, Response) { + let mut deps = mock_dependencies(); + + let res = instantiate( + deps.as_mut(), + mock_env(), + mock_info(DEPLOYER, &[]), + InstantiateMsg { + token: token_mode_collateral, + hrp: hrp.to_string(), + owner: OWNER.to_string(), + mailbox: MAILBOX.to_string(), + }, + ) + .unwrap(); + + if let Some(token) = token { + super::TOKEN + .save(deps.as_mut().storage, &addr(token)) + .unwrap(); + } + + if !routes.is_empty() { + set_routes( + deps.as_mut().storage, + &addr(OWNER), + routes + .into_iter() + .map(|v| DomainRouteSet { + domain: v.0, + route: Some(v.1), + }) + .collect(), + ) + .unwrap(); + } + + (deps, res) + } + + #[rstest] + #[case(token_mode_bridged())] + #[case(token_mode_collateral())] + fn test_queries(#[values("osmo", "neutron")] hrp: &str, #[case] token_mode: Cw20TokenMode) { + let (deps, _) = deps(vec![], hrp, Some(TOKEN), token_mode.clone()); + + let res: warp::TokenTypeResponse = test_query( + deps.as_ref(), + QueryMsg::TokenDefault(warp::TokenWarpDefaultQueryMsg::TokenType {}), + ); + assert_eq!( + res.typ, + warp::TokenType::CW20 { + contract: TOKEN.into() + } + ); + + let res: warp::TokenModeResponse = test_query( + deps.as_ref(), + QueryMsg::TokenDefault(warp::TokenWarpDefaultQueryMsg::TokenMode {}), + ); + assert_eq!(res.mode, token_mode.into()); + } + + #[rstest] + #[case(token_mode_bridged())] + #[case(token_mode_collateral())] + fn test_init(#[values("osmo", "neutron")] hrp: &str, #[case] token_mode: Cw20TokenMode) { + let (deps, res) = deps(vec![], hrp, None, token_mode.clone()); + + let storage = deps.as_ref().storage; + let mode = token_mode.clone().into(); + + assert_eq!(super::HRP.load(storage).unwrap(), hrp); + assert_eq!(super::MODE.load(storage).unwrap(), mode); + assert_eq!(super::MAILBOX.load(storage).unwrap(), MAILBOX); + + match token_mode { + TokenModeMsg::Bridged(mut v) => { + v.init_msg.mint = Some(cw20::MinterResponse { + minter: mock_env().contract.address.into(), + cap: None, + }); + + assert!(!super::TOKEN.exists(storage)); + + let reply = res.messages.get(0).unwrap(); + assert_eq!(reply.id, REPLY_ID_CREATE_DENOM); + assert_eq!( + reply.msg, + CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(mock_env().contract.address.to_string()), + code_id: v.code_id, + msg: cosmwasm_std::to_binary(&v.init_msg).unwrap(), + funds: vec![], + label: "token warp cw20".to_string() + }) + ) + } + TokenModeMsg::Collateral(v) => { + assert_eq!(super::TOKEN.load(storage).unwrap(), v.address); + assert!(res.messages.is_empty()) + } + } + } + + #[rstest] + #[case(MAILBOX, 1, gen_bz(32), token_mode_bridged())] + #[case(MAILBOX, 1, gen_bz(32), token_mode_collateral())] + #[should_panic(expected = "unauthorized")] + #[case(TOKEN, 1, gen_bz(32), token_mode_collateral())] + #[should_panic(expected = "route not found")] + #[case(MAILBOX, 2, gen_bz(32), token_mode_collateral())] + fn test_mailbox_handle( + #[values("osmo", "neutron")] hrp: &str, + #[case] sender: &str, + #[case] domain: u32, + #[case] route: HexBinary, + #[case] token_mode: Cw20TokenMode, + ) { + let (mut deps, _) = deps( + vec![(1, route.clone())], + hrp, + Some(TOKEN), + token_mode.clone(), + ); + + let warp_msg = warp::Message { + recipient: gen_bz(32), + amount: Uint256::from_u128(100), + metadata: HexBinary::default(), + }; + + let handle_msg = HandleMsg { + origin: domain, + sender: route, + body: warp_msg.clone().into(), + }; + + let res = test_execute( + deps.as_mut(), + &addr(sender), + ExecuteMsg::Handle(handle_msg), + vec![], + ); + let msg = &res.messages.get(0).unwrap().msg; + + match token_mode { + TokenModeMsg::Bridged(_) => { + assert_eq!( + cosmwasm_std::to_binary(msg).unwrap(), + cosmwasm_std::to_binary(&CosmosMsg::::Wasm( + conv::to_mint_msg( + TOKEN, + bech32_encode(hrp, warp_msg.recipient.as_slice()).unwrap(), + warp_msg.amount + ) + .unwrap() + )) + .unwrap() + ) + } + TokenModeMsg::Collateral(_) => { + assert_eq!( + cosmwasm_std::to_binary(msg).unwrap(), + cosmwasm_std::to_binary(&CosmosMsg::::Wasm( + conv::to_send_msg( + TOKEN, + bech32_encode(hrp, warp_msg.recipient.as_slice()).unwrap(), + warp_msg.amount + ) + .unwrap() + )) + .unwrap() + ); + } + } + } + + #[rstest] + #[case(1, gen_bz(32), token_mode_bridged())] + #[case(1, gen_bz(32), token_mode_collateral())] + #[should_panic(expected = "route not found")] + #[case(2, gen_bz(32), token_mode_collateral())] + fn test_transfer_remote( + #[values("osmo", "neutron")] hrp: &str, + #[case] domain: u32, + #[case] route: HexBinary, + #[case] token_mode: Cw20TokenMode, + ) { + let (mut deps, _) = deps( + vec![(1, route.clone())], + hrp, + Some(TOKEN), + token_mode.clone(), + ); + + let sender = addr("sender"); + let recipient = gen_bz(32); + + let res = test_execute( + deps.as_mut(), + &sender, + ExecuteMsg::TransferRemote { + dest_domain: domain, + recipient: recipient.clone(), + amount: Uint128::new(100), + }, + vec![], + ); + let msgs = res.messages.into_iter().map(|v| v.msg).collect::>(); + + let transfer_from_msg = wasm_execute( + TOKEN, + &Cw20ExecuteMsg::TransferFrom { + owner: sender.to_string(), + recipient: mock_env().contract.address.to_string(), + amount: Uint128::new(100), + }, + vec![], + ) + .unwrap(); + + let warp_msg = warp::Message { + recipient: recipient, + amount: Uint256::from_u128(100), + metadata: HexBinary::default(), + }; + + let dispatch_msg = + mailbox::dispatch(MAILBOX, domain, route, warp_msg.into(), None, None, vec![]).unwrap(); + + match token_mode { + TokenModeMsg::Bridged(_) => { + assert_eq!( + cosmwasm_std::to_binary(&msgs).unwrap(), + cosmwasm_std::to_binary(&vec![ + transfer_from_msg.into(), + CosmosMsg::from(conv::to_burn_msg(TOKEN, Uint128::new(100)).unwrap()), + dispatch_msg, + ]) + .unwrap(), + ); + } + TokenModeMsg::Collateral(_) => { + assert_eq!( + cosmwasm_std::to_binary(&msgs).unwrap(), + cosmwasm_std::to_binary(&vec![transfer_from_msg.into(), dispatch_msg]).unwrap(), + ); + } + } + } +} diff --git a/contracts/warp/cw20/src/error.rs b/contracts/warp/cw20/src/error.rs index a4fef9b4..fc498fa4 100644 --- a/contracts/warp/cw20/src/error.rs +++ b/contracts/warp/cw20/src/error.rs @@ -9,21 +9,21 @@ pub enum ContractError { #[error("{0}")] ParseReplyError(#[from] cw_utils::ParseReplyError), - #[error("Unauthorized")] + #[error("unauthorized")] Unauthorized, - #[error("WrongLength")] + #[error("wrong length")] WrongLength {}, - #[error("InvalidTokenOption")] + #[error("invalid token option")] InvalidTokenOption, - #[error("InvalidReplyId")] + #[error("invalid reply id")] InvalidReplyId, - #[error("InvalidReceiveMsg")] + #[error("invalid receive msg")] InvalidReceiveMsg, - #[error("NoRouter domain:{domain:?}")] + #[error("no router for domain {domain:?}")] NoRouter { domain: u32 }, } diff --git a/contracts/warp/cw20/src/lib.rs b/contracts/warp/cw20/src/lib.rs index 02e02014..0924bda9 100644 --- a/contracts/warp/cw20/src/lib.rs +++ b/contracts/warp/cw20/src/lib.rs @@ -6,9 +6,6 @@ pub mod contract; mod conv; pub mod error; -#[cfg(test)] -mod tests; - // reply message pub const REPLY_ID_CREATE_DENOM: u64 = 0; diff --git a/contracts/warp/cw20/src/tests/contracts.rs b/contracts/warp/cw20/src/tests/contracts.rs deleted file mode 100644 index dbf24122..00000000 --- a/contracts/warp/cw20/src/tests/contracts.rs +++ /dev/null @@ -1,292 +0,0 @@ -// use std::str::FromStr; - -// use cosmwasm_std::{testing::mock_env, to_binary, Addr, Binary, CosmosMsg, Uint256, WasmMsg}; -// use hpl_interface::{ -// types::bech32_encode, -// warp::{self, cw20::TokenOption}, -// }; -// use rstest::rstest; - -// use crate::{error::ContractError, tests::TokenCW20, TOKEN}; - -// #[rstest] -// #[case("osmo")] -// #[case("neutron")] -// fn test_router_role(#[case] hrp: &str) -> anyhow::Result<()> { -// let deployer = Addr::unchecked("deployer"); -// let mailbox = Addr::unchecked("mailbox"); -// let owner = Addr::unchecked("owner"); - -// let token = Addr::unchecked("token-native"); -// let domain = 999; -// let router = Binary(b"hello".to_vec()); - -// let mut warp = TokenCW20::default(); - -// warp.init( -// &deployer, -// &owner, -// &mailbox, -// Some(TokenOption::Reuse { -// contract: token.to_string(), -// }), -// TokenMode::Bridged, -// hrp, -// )?; - -// // err -// let err = warp -// .router_enroll(&mailbox, domain, router.clone()) -// .unwrap_err(); -// assert_eq!(err, ContractError::Unauthorized); - -// // ok -// warp.router_enroll(&owner, domain, router)?; - -// Ok(()) -// } - -// #[rstest] -// #[case("osmo")] -// #[case("neutron")] -// fn test_outbound_transfer(#[case] hrp: &str) -> anyhow::Result<()> { -// let deployer = Addr::unchecked("deployer"); -// let mailbox = Addr::unchecked("mailbox"); -// let router = Addr::unchecked("router"); -// let owner = Addr::unchecked("owner"); - -// let token = Addr::unchecked("token-cw20"); -// let amount: u64 = 100_000; - -// let user_remote = Addr::unchecked("user-remote____________________1"); - -// let dest_domain = 1; - -// let env = mock_env(); - -// let burn_msg: CosmosMsg = WasmMsg::Execute { -// contract_addr: token.to_string(), -// msg: to_binary(&cw20::Cw20ExecuteMsg::Burn { -// amount: amount.into(), -// })?, -// funds: vec![], -// } -// .into(); - -// let dispatch_msg: CosmosMsg = WasmMsg::Execute { -// contract_addr: mailbox.to_string(), -// msg: to_binary(&mailbox::ExecuteMsg::Dispatch { -// dest_domain, -// recipient_addr: Binary(router.as_bytes().to_vec()).into(), -// msg_body: token::Message { -// recipient: Binary(user_remote.as_bytes().to_vec()), -// amount: Uint256::from_str(&amount.to_string())?, -// metadata: Binary::default(), -// } -// .into(), -// })?, -// funds: vec![], -// } -// .into(); - -// for (mode, routers, expected_resp) in [ -// ( -// TokenMode::Bridged, -// vec![(dest_domain, Binary(router.as_bytes().to_vec()))], -// Ok(vec![burn_msg, dispatch_msg.clone()]), -// ), -// ( -// TokenMode::Bridged, -// vec![], -// Err(ContractError::NoRouter { -// domain: dest_domain, -// }), -// ), -// ( -// TokenMode::Collateral, -// vec![(dest_domain, Binary(router.as_bytes().to_vec()))], -// Ok(vec![dispatch_msg]), -// ), -// ( -// TokenMode::Collateral, -// vec![], -// Err(ContractError::NoRouter { -// domain: dest_domain, -// }), -// ), -// ] { -// let mut warp = TokenCW20 { -// env: env.clone(), -// ..Default::default() -// }; - -// warp.init( -// &deployer, -// &owner, -// &mailbox, -// Some(TokenOption::Reuse { -// contract: token.to_string(), -// }), -// mode.clone(), -// hrp, -// )?; -// if mode == TokenMode::Collateral { -// TOKEN.save(&mut warp.deps.storage, &token)?; -// } - -// for (domain, router) in routers { -// warp.router_enroll(&owner, domain, router)?; -// } - -// let resp = warp.transfer_remote( -// &deployer, -// &token, -// amount.into(), -// dest_domain, -// user_remote.as_bytes().into(), -// ); - -// assert_eq!( -// resp.map(|v| v.messages.into_iter().map(|v| v.msg).collect::>()), -// expected_resp -// ); -// } - -// Ok(()) -// } - -// #[rstest] -// #[case("osmo")] -// #[case("neutron")] -// fn test_inbound_transfer(#[case] hrp: &str) -> anyhow::Result<()> { -// let deployer = Addr::unchecked("deployer"); -// let mailbox = Addr::unchecked("mailbox"); -// let router = Addr::unchecked("router"); -// let owner = Addr::unchecked("owner"); -// let errortic = Addr::unchecked("errortic"); - -// let token = Addr::unchecked("token-cw20"); -// let amount = 100_000; - -// let user_remote = Addr::unchecked("user-remote____________________1"); - -// let env = mock_env(); - -// let origin_domain = 1; - -// let mint_msg: CosmosMsg = WasmMsg::Execute { -// contract_addr: token.to_string(), -// msg: to_binary(&cw20::Cw20ExecuteMsg::Mint { -// recipient: bech32_encode(hrp, user_remote.as_bytes())?.to_string(), -// amount: amount.into(), -// })?, -// funds: vec![], -// } -// .into(); - -// let send_msg: CosmosMsg = WasmMsg::Execute { -// contract_addr: token.to_string(), -// msg: to_binary(&cw20::Cw20ExecuteMsg::Transfer { -// recipient: bech32_encode(hrp, user_remote.as_bytes())?.to_string(), -// amount: amount.into(), -// })?, -// funds: vec![], -// } -// .into(); - -// let default_msg = token::Message { -// recipient: user_remote.as_bytes().to_vec().into(), -// amount: Uint256::from_u128(amount), -// metadata: Binary::default(), -// }; - -// for (mode, sender, origin, origin_sender, token_msg, expected_resp) in [ -// // happy -// ( -// TokenMode::Bridged, -// &mailbox, -// origin_domain, -// &router, -// default_msg.clone(), -// Ok(vec![mint_msg]), -// ), -// ( -// TokenMode::Collateral, -// &mailbox, -// origin_domain, -// &router, -// default_msg.clone(), -// Ok(vec![send_msg]), -// ), -// // errors -// ( -// TokenMode::Bridged, -// &errortic, -// origin_domain, -// &router, -// default_msg.clone(), -// Err(ContractError::Unauthorized), -// ), -// ( -// TokenMode::Bridged, -// &mailbox, -// origin_domain, -// &errortic, -// default_msg.clone(), -// Err(ContractError::Unauthorized), -// ), -// ( -// TokenMode::Collateral, -// &errortic, -// origin_domain, -// &router, -// default_msg.clone(), -// Err(ContractError::Unauthorized), -// ), -// ( -// TokenMode::Collateral, -// &mailbox, -// origin_domain, -// &errortic, -// default_msg, -// Err(ContractError::Unauthorized), -// ), -// ] { -// let mut warp = TokenCW20 { -// env: env.clone(), -// ..Default::default() -// }; - -// warp.init( -// &deployer, -// &owner, -// &mailbox, -// Some(TokenOption::Reuse { -// contract: token.to_string(), -// }), -// mode.clone(), -// hrp, -// )?; -// if mode == TokenMode::Collateral { -// TOKEN.save(&mut warp.deps.storage, &token)?; -// } - -// warp.router_enroll(&owner, origin_domain, router.as_bytes().into())?; - -// let resp = warp.mailbox_handle( -// sender, -// mailbox::HandleMsg { -// origin, -// sender: origin_sender.as_bytes().to_vec().into(), -// body: token_msg.into(), -// }, -// ); - -// assert_eq!( -// resp.map(|v| v.messages.into_iter().map(|v| v.msg).collect::>()), -// expected_resp -// ); -// } - -// Ok(()) -// } diff --git a/contracts/warp/cw20/src/tests/mod.rs b/contracts/warp/cw20/src/tests/mod.rs deleted file mode 100644 index e26b05af..00000000 --- a/contracts/warp/cw20/src/tests/mod.rs +++ /dev/null @@ -1,115 +0,0 @@ -// use cosmwasm_std::{ -// from_binary, -// testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage}, -// to_binary, Addr, Binary, Empty, Env, MessageInfo, OwnedDeps, Response, Uint128, -// }; -// use hpl_interface::{ -// mailbox, router, -// token::TokenMode, -// token_cw20::{ExecuteMsg, QueryMsg, ReceiveMsg}, -// }; -// use serde::de::DeserializeOwned; - -// use crate::{ -// contract::{execute, instantiate, query}, -// error::ContractError, -// msg::{InstantiateMsg, TokenOption}, -// }; - -// mod contracts; - -// pub struct TokenCW20 { -// pub deps: OwnedDeps, -// pub env: Env, -// } - -// impl Default for TokenCW20 { -// fn default() -> Self { -// Self { -// deps: mock_dependencies(), -// env: mock_env(), -// } -// } -// } - -// impl TokenCW20 { -// pub fn init( -// &mut self, -// sender: &Addr, -// owner: &Addr, -// mailbox: &Addr, -// token: Option, -// mode: TokenMode, -// hrp: &str, -// ) -> Result { -// instantiate( -// self.deps.as_mut(), -// self.env.clone(), -// mock_info(sender.as_str(), &[]), -// InstantiateMsg { -// token, -// mode, -// hrp: hrp.to_string(), -// owner: owner.to_string(), -// mailbox: mailbox.to_string(), -// }, -// ) -// } - -// fn execute(&mut self, info: MessageInfo, msg: ExecuteMsg) -> Result { -// execute(self.deps.as_mut(), self.env.clone(), info, msg) -// } - -// #[allow(dead_code)] -// fn query(&self, msg: QueryMsg) -> Result { -// query(self.deps.as_ref(), self.env.clone(), msg) -// .map(|v| from_binary::(&v))? -// .map_err(|e| e.into()) -// } - -// pub fn router_enroll( -// &mut self, -// sender: &Addr, -// domain: u32, -// router: Binary, -// ) -> Result { -// self.execute( -// mock_info(sender.as_str(), &[]), -// ExecuteMsg::Router(router::RouterMsg::EnrollRemoteRouter { -// set: router::RouterSet { domain, router }, -// }), -// ) -// } - -// pub fn mailbox_handle( -// &mut self, -// sender: &Addr, -// handle_msg: mailbox::HandleMsg, -// ) -> Result { -// self.execute( -// mock_info(sender.as_str(), &[]), -// ExecuteMsg::Handle(handle_msg), -// ) -// } - -// pub fn transfer_remote( -// &mut self, -// sender: &Addr, -// token: &Addr, -// amount: Uint128, -// dest_domain: u32, -// recipient: Binary, -// ) -> Result { -// self.execute( -// mock_info(token.as_str(), &[]), -// ExecuteMsg::Receive(cw20::Cw20ReceiveMsg { -// sender: sender.to_string(), -// amount, -// msg: to_binary(&ReceiveMsg::TransferRemote { -// dest_domain, -// recipient, -// })?, -// }), -// ) -// } -// } diff --git a/contracts/warp/native/Cargo.toml b/contracts/warp/native/Cargo.toml index c0b218e1..f621ef34 100644 --- a/contracts/warp/native/Cargo.toml +++ b/contracts/warp/native/Cargo.toml @@ -39,11 +39,15 @@ serde-json-wasm.workspace = true thiserror.workspace = true +hpl-connection.workspace = true hpl-ownable.workspace = true hpl-router.workspace = true hpl-interface.workspace = true [dev-dependencies] +serde-json-wasm.workspace = true + +ibcx-test-utils.workspace = true rstest.workspace = true anyhow.workspace = true k256.workspace = true diff --git a/contracts/warp/native/src/contract.rs b/contracts/warp/native/src/contract.rs index 2c71d8c4..da89fa0d 100644 --- a/contracts/warp/native/src/contract.rs +++ b/contracts/warp/native/src/contract.rs @@ -1,11 +1,13 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - ensure_eq, CosmosMsg, Deps, DepsMut, Env, HexBinary, MessageInfo, QueryResponse, Reply, - Response, SubMsg, Uint256, + ensure, ensure_eq, CosmosMsg, Deps, DepsMut, Empty, Env, HexBinary, MessageInfo, QueryResponse, + Reply, Response, SubMsg, Uint128, Uint256, }; +use hpl_connection::{get_hook, get_ism}; use hpl_interface::{ core::mailbox, + ism::{InterchainSecurityModuleResponse, IsmSpecifierQueryMsg}, to_binary, types::bech32_encode, warp::{ @@ -42,9 +44,7 @@ pub fn instantiate( hpl_ownable::initialize(deps.storage, &owner)?; - let mut denom = "".into(); - - let msgs = match msg.token { + let (msgs, denom) = match msg.token { // create native denom if token is bridged TokenModeMsg::Bridged(token) => { let mut msgs = vec![]; @@ -64,13 +64,12 @@ pub fn instantiate( ))); } - msgs + (msgs, token.denom) } // use denom directly if token is native TokenModeMsg::Collateral(token) => { TOKEN.save(deps.storage, &token.denom)?; - denom = token.denom; - vec![] + (vec![], token.denom) } }; @@ -90,13 +89,18 @@ pub fn execute( info: MessageInfo, msg: ExecuteMsg, ) -> Result { + use ExecuteMsg::*; + match msg { - ExecuteMsg::Router(msg) => Ok(hpl_router::handle(deps, env, info, msg)?), - ExecuteMsg::Handle(msg) => mailbox_handle(deps, env, info, msg), - ExecuteMsg::TransferRemote { + Ownable(msg) => Ok(hpl_ownable::handle(deps, env, info, msg)?), + Router(msg) => Ok(hpl_router::handle(deps, env, info, msg)?), + Connection(msg) => Ok(hpl_connection::handle(deps, env, info, msg)?), + Handle(msg) => mailbox_handle(deps, env, info, msg), + TransferRemote { dest_domain, recipient, - } => transfer_remote(deps, env, info, dest_domain, recipient), + amount, + } => transfer_remote(deps, env, info, dest_domain, recipient, amount), } } @@ -110,9 +114,8 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result Result { let token = TOKEN.load(deps.storage)?; let mode = MODE.load(deps.storage)?; let mailbox = MAILBOX.load(deps.storage)?; - let transfer_amount = cw_utils::must_pay(&info, &token)?; + + let mut funds = info.funds.clone(); + + let (token_index, token_received) = funds + .iter() + .enumerate() + .find(|(_, v)| v.denom == token) + .expect("no funds sent"); + ensure!( + token_received.amount >= transfer_amount, + ContractError::InsufficientFunds + ); + + funds[token_index].amount -= transfer_amount; let dest_router = get_route::(deps.storage, dest_domain)? .route @@ -207,8 +224,9 @@ fn transfer_remote( dest_domain, dest_router, dispatch_payload.into(), + get_hook(deps.storage)?.map(|v| v.into()), None, - None, + funds, )?); Ok(Response::new().add_messages(msgs).add_event( @@ -227,10 +245,16 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result Ok(hpl_ownable::handle_query(deps, env, msg)?), QueryMsg::Router(msg) => Ok(hpl_router::handle_query(deps, env, msg)?), + QueryMsg::Connection(msg) => Ok(hpl_connection::handle_query(deps, env, msg)?), QueryMsg::TokenDefault(msg) => match msg { TokenType {} => to_binary(get_token_type(deps)), TokenMode {} => to_binary(get_token_mode(deps)), }, + QueryMsg::IsmSpecifier(IsmSpecifierQueryMsg::InterchainSecurityModule()) => Ok( + cosmwasm_std::to_binary(&InterchainSecurityModuleResponse { + ism: get_ism(deps.storage)?, + })?, + ), } } @@ -247,3 +271,305 @@ fn get_token_mode(deps: Deps) -> Result { Ok(TokenModeResponse { mode }) } + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: Empty) -> Result { + Ok(Response::new()) +} + +#[cfg(test)] +mod test { + use cosmwasm_std::{ + coin, + testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage}, + Coin, OwnedDeps, Uint128, + }; + use hpl_interface::{ + build_test_executor, build_test_querier, + core::HandleMsg, + router::DomainRouteSet, + warp::native::{Metadata, NativeModeBriged, NativeModeCollateral}, + }; + use hpl_router::set_route; + use ibcx_test_utils::{addr, gen_bz}; + use rstest::{fixture, rstest}; + + use super::*; + + build_test_executor!(super::execute); + build_test_querier!(super::query); + + type NativeTokenMode = TokenModeMsg; + type TestDeps = OwnedDeps; + + const DEPLOYER: &str = "deployer"; + const OWNER: &str = "owner"; + const MAILBOX: &str = "mailbox"; + const DENOM: &str = "utest"; + + #[fixture] + fn metadata(#[default(true)] empty: bool) -> Option { + if empty { + None + } else { + Some(Metadata { + description: "testtesttest".into(), + denom_units: vec![], + base: "basebasebase".into(), + display: "displaydisplaydisplay".into(), + name: DENOM.into(), + symbol: DENOM.into(), + }) + } + } + + #[fixture] + fn token_mode_bridged(metadata: Option) -> NativeTokenMode { + TokenModeMsg::Bridged(NativeModeBriged { + denom: DENOM.into(), + metadata, + }) + } + + #[fixture] + fn token_mode_collateral() -> NativeTokenMode { + TokenModeMsg::Collateral(NativeModeCollateral { + denom: DENOM.into(), + }) + } + + #[fixture] + fn deps( + #[default(token_mode_collateral())] token_mode: NativeTokenMode, + #[default("osmo")] hrp: &str, + ) -> TestDeps { + let mut deps = mock_dependencies(); + + super::instantiate( + deps.as_mut(), + mock_env(), + mock_info(DEPLOYER, &[]), + super::InstantiateMsg { + token: token_mode, + hrp: hrp.into(), + owner: OWNER.into(), + mailbox: MAILBOX.into(), + }, + ) + .unwrap(); + + deps + } + + #[rstest] + #[case(token_mode_bridged(metadata(true)))] + #[case(token_mode_bridged(metadata(false)))] + #[case(token_mode_collateral())] + fn test_queries(#[values("osmo", "neutron")] hrp: &str, #[case] token_mode: NativeTokenMode) { + let mut deps = deps(token_mode.clone(), hrp); + + if TokenMode::from(token_mode.clone()) == TokenMode::Bridged { + super::TOKEN + .save(deps.as_mut().storage, &DENOM.into()) + .unwrap(); + } + + let res: warp::TokenTypeResponse = test_query( + deps.as_ref(), + QueryMsg::TokenDefault(warp::TokenWarpDefaultQueryMsg::TokenType {}), + ); + assert_eq!( + res.typ, + warp::TokenType::Native(warp::TokenTypeNative::Fungible { + denom: DENOM.into() + }) + ); + + let res: warp::TokenModeResponse = test_query( + deps.as_ref(), + QueryMsg::TokenDefault(warp::TokenWarpDefaultQueryMsg::TokenMode {}), + ); + assert_eq!(res.mode, token_mode.into()); + } + + #[rstest] + #[case(token_mode_bridged(metadata(true)))] + #[case(token_mode_bridged(metadata(false)))] + #[case(token_mode_collateral())] + fn test_init(#[values("osmo", "neutron")] hrp: &str, #[case] token_mode: NativeTokenMode) { + let mut deps = mock_dependencies(); + + let res = super::instantiate( + deps.as_mut(), + mock_env(), + mock_info(DEPLOYER, &[]), + super::InstantiateMsg { + token: token_mode.clone(), + hrp: hrp.into(), + owner: OWNER.into(), + mailbox: MAILBOX.into(), + }, + ) + .unwrap(); + + let storage = deps.as_ref().storage; + assert_eq!(super::HRP.load(storage).unwrap(), hrp); + assert_eq!( + super::MODE.load(storage).unwrap(), + token_mode.clone().into() + ); + assert_eq!(super::MAILBOX.load(storage).unwrap(), MAILBOX); + + match token_mode { + TokenModeMsg::Bridged(v) => { + if v.metadata.is_some() { + assert_eq!(res.messages.len(), 2); + } else { + assert_eq!(res.messages.len(), 1); + } + } + TokenModeMsg::Collateral(_) => { + assert_eq!(res.messages.len(), 0); + assert_eq!(super::TOKEN.load(storage).unwrap(), DENOM); + } + } + } + + #[rstest] + #[case(MAILBOX, 1, gen_bz(32))] + #[should_panic(expected = "unauthorized")] + #[case(OWNER, 1, gen_bz(32))] + #[should_panic(expected = "route not found")] + #[case(MAILBOX, 2, gen_bz(32))] + fn test_mailbox_handle( + mut deps: TestDeps, + #[case] sender: &str, + #[case] origin_domain: u32, + #[case] origin_sender: HexBinary, + ) { + let recipient = gen_bz(32); + + let handle_msg = HandleMsg { + origin: origin_domain, + sender: origin_sender.clone(), + body: warp::Message { + recipient: recipient.clone(), + amount: Uint256::from_u128(100), + metadata: HexBinary::default(), + } + .into(), + }; + + set_route( + deps.as_mut().storage, + &addr(OWNER), + DomainRouteSet { + domain: 1, + route: Some(origin_sender), + }, + ) + .unwrap(); + + let res = test_execute( + deps.as_mut(), + &addr(sender), + ExecuteMsg::Handle(handle_msg), + vec![], + ); + let mut msgs: Vec<_> = res.messages.into_iter().map(|v| v.msg).collect(); + + let mode = MODE.load(deps.as_ref().storage).unwrap(); + + assert_eq!( + msgs.pop().unwrap(), + conv::to_send_msg( + &bech32_encode("osmo", recipient.as_slice()).unwrap(), + vec![coin(100, DENOM)] + ) + .into() + ); + + if mode == TokenMode::Bridged { + assert_eq!( + msgs.pop().unwrap(), + conv::to_mint_msg(&mock_env().contract.address, DENOM, "100").into() + ); + } else { + assert!(msgs.is_empty()); + } + } + + #[rstest] + #[case(1, gen_bz(32), gen_bz(32), vec![coin(100, DENOM)])] + #[case(1, gen_bz(32), gen_bz(32), vec![coin(100, DENOM), coin(100, "uatom")])] + #[should_panic(expected = "route not found")] + #[case(2, gen_bz(32), gen_bz(32), vec![coin(100, DENOM)])] + #[should_panic(expected = "no funds sent")] + #[case(1, gen_bz(32), gen_bz(32), vec![])] + #[should_panic(expected = "no funds sent")] + #[case(1, gen_bz(32), gen_bz(32), vec![coin(100, "uatom")])] + fn test_transfer_remote( + mut deps: TestDeps, + #[case] dest_domain: u32, + #[case] dest_router: HexBinary, + #[case] dest_recipient: HexBinary, + #[case] funds: Vec, + ) { + set_route( + deps.as_mut().storage, + &addr(OWNER), + DomainRouteSet { + domain: 1, + route: Some(dest_router.clone()), + }, + ) + .unwrap(); + + let res = test_execute( + deps.as_mut(), + &addr("sender"), + ExecuteMsg::TransferRemote { + dest_domain, + recipient: dest_recipient.clone(), + amount: Uint128::new(50), + }, + funds.clone(), + ); + let mut msgs: Vec<_> = res.messages.into_iter().map(|v| v.msg).collect(); + + let mode = MODE.load(deps.as_ref().storage).unwrap(); + + assert_eq!( + msgs.last().unwrap(), + &mailbox::dispatch( + MAILBOX, + dest_domain, + dest_router, + warp::Message { + recipient: dest_recipient, + amount: Uint256::from_u128(50), + metadata: HexBinary::default(), + } + .into(), + None, + None, + vec![ + vec![coin(50, DENOM)], + funds.into_iter().filter(|v| v.denom != DENOM).collect() + ] + .concat() + ) + .unwrap() + ); + msgs.remove(msgs.len() - 1); // remove last (dispatch) msg + + if mode == TokenMode::Bridged { + assert_eq!( + msgs.pop().unwrap(), + conv::to_burn_msg(&mock_env().contract.address, DENOM, "100").into() + ); + } else { + assert!(msgs.is_empty()); + } + } +} diff --git a/contracts/warp/native/src/error.rs b/contracts/warp/native/src/error.rs index e0715bce..18dd1b41 100644 --- a/contracts/warp/native/src/error.rs +++ b/contracts/warp/native/src/error.rs @@ -12,15 +12,18 @@ pub enum ContractError { #[error("{0}")] RecoverPubkeyError(#[from] RecoverPubkeyError), - #[error("Unauthorized")] + #[error("unauthorized")] Unauthorized, - #[error("WrongLength")] + #[error("wrong length")] WrongLength, - #[error("InvalidReplyId")] + #[error("invalid reply id")] InvalidReplyId, - #[error("NoRouter domain:{domain:?}")] + #[error("insufficient funds")] + InsufficientFunds, + + #[error("no route for domain {domain:?}")] NoRouter { domain: u32 }, } diff --git a/contracts/warp/native/src/lib.rs b/contracts/warp/native/src/lib.rs index 426030f8..3b625710 100644 --- a/contracts/warp/native/src/lib.rs +++ b/contracts/warp/native/src/lib.rs @@ -7,9 +7,6 @@ mod conv; pub mod error; mod proto; -#[cfg(test)] -mod tests; - // reply message pub const REPLY_ID_CREATE_DENOM: u64 = 0; diff --git a/contracts/warp/native/src/tests/contracts.rs b/contracts/warp/native/src/tests/contracts.rs deleted file mode 100644 index c7d92baa..00000000 --- a/contracts/warp/native/src/tests/contracts.rs +++ /dev/null @@ -1,279 +0,0 @@ -// use std::str::FromStr; - -// use cosmwasm_std::{ -// coin, testing::mock_env, to_binary, Addr, BankMsg, CosmosMsg, HexBinary, Uint256, WasmMsg, -// }; -// use hpl_interface::{ -// core::mailbox, -// types::bech32_encode, -// warp::{self, TokenMode}, -// }; -// use rstest::rstest; - -// use crate::{ -// error::ContractError, -// proto::{self, MsgBurn, MsgMint}, -// }; - -// use super::TokenNative; - -// #[rstest] -// #[case("osmo")] -// #[case("neutron")] -// fn test_init(#[case] hrp: &str) -> anyhow::Result<()> { -// let deployer = Addr::unchecked("deployer"); -// let mailbox = Addr::unchecked("mailbox"); -// let owner = Addr::unchecked("owner"); - -// let mut warp = TokenNative::default(); - -// warp.init( -// &deployer, -// hrp, -// &owner, -// &mailbox, -// "token-warp", -// None, -// TokenMode::Bridged, -// )?; - -// Ok(()) -// } - -// #[rstest] -// #[case("osmo")] -// #[case("neutron")] -// fn test_router_role(#[case] hrp: &str) -> anyhow::Result<()> { -// let deployer = Addr::unchecked("deployer"); -// let mailbox = Addr::unchecked("mailbox"); -// let owner = Addr::unchecked("owner"); - -// let denom = "token-native"; -// let domain = 999; -// let router = b"hello".to_vec(); - -// let mut warp = TokenNative::default(); - -// warp.init_hack(&deployer, &owner, &mailbox, hrp, denom, TokenMode::Bridged)?; - -// // err -// let err = warp -// .router_enroll(&mailbox, domain, router.clone()) -// .unwrap_err(); -// assert_eq!(err, ContractError::Unauthorized); - -// // ok -// warp.router_enroll(&owner, domain, router)?; - -// Ok(()) -// } - -// #[rstest] -// fn test_outbound_transfer(#[values("osmo", "neutron")] hrp: &str) -> anyhow::Result<()> { -// let deployer = Addr::unchecked("deployer"); -// let mailbox = Addr::unchecked("mailbox"); -// let router = Addr::unchecked("router"); -// let owner = Addr::unchecked("owner"); - -// let denom = "token-native"; -// let amount = 100_000; - -// let user_remote = Addr::unchecked("user-remote"); - -// let dest_domain = 1; - -// let env = mock_env(); - -// let burn_msg: CosmosMsg = MsgBurn { -// sender: env.contract.address.to_string(), -// amount: Some(proto::Coin { -// amount: amount.to_string(), -// denom: denom.to_string(), -// }), -// } -// .into(); - -// let dispatch_msg = mailbox::dispatch( -// mailbox, -// dest_domain, -// router.as_bytes().to_vec().into(), -// warp::Message { -// recipient: user_remote.as_bytes().to_vec().into(), -// amount: Uint256::from_str(&amount.to_string())?, -// metadata: HexBinary::default(), -// } -// .into(), -// None, -// None, -// ); - -// for (mode, routers, expected_resp) in [ -// ( -// TokenMode::Bridged, -// vec![(dest_domain, router.as_bytes().into())], -// Ok(vec![burn_msg, dispatch_msg.clone()]), -// ), -// ( -// TokenMode::Bridged, -// vec![], -// Err(ContractError::NoRouter { -// domain: dest_domain, -// }), -// ), -// ( -// TokenMode::Collateral, -// vec![(dest_domain, router.as_bytes().into())], -// Ok(vec![dispatch_msg]), -// ), -// ( -// TokenMode::Collateral, -// vec![], -// Err(ContractError::NoRouter { -// domain: dest_domain, -// }), -// ), -// ] { -// let mut warp = TokenNative { -// env: env.clone(), -// ..Default::default() -// }; - -// warp.init_hack(&deployer, &owner, &mailbox, hrp, denom, mode)?; - -// for (domain, router) in routers { -// warp.router_enroll(&owner, domain, router)?; -// } - -// let resp = warp.transfer_remote( -// &owner, -// coin(amount, denom), -// dest_domain, -// user_remote.as_bytes().into(), -// ); - -// assert_eq!( -// resp.map(|v| v.messages.into_iter().map(|v| v.msg).collect::>()), -// expected_resp -// ); -// } - -// Ok(()) -// } - -// #[rstest] -// #[case("osmo")] -// #[case("neutron")] -// fn test_inbound_transfer(#[case] hrp: &str) -> anyhow::Result<()> { -// let deployer = Addr::unchecked("deployer"); -// let mailbox = Addr::unchecked("mailbox"); -// let router = Addr::unchecked("router"); -// let owner = Addr::unchecked("owner"); -// let errortic = Addr::unchecked("errortic"); - -// let denom = "token-native"; -// let amount = 100_000; - -// let user_remote = Addr::unchecked("user-remote____________________1"); - -// let env = mock_env(); - -// let origin_domain = 1; - -// let mint_msg: CosmosMsg = MsgMint { -// sender: env.contract.address.to_string(), -// amount: Some(proto::Coin { -// amount: amount.to_string(), -// denom: denom.to_string(), -// }), -// } -// .into(); - -// let send_msg: CosmosMsg = BankMsg::Send { -// to_address: bech32_encode(hrp, user_remote.as_bytes())?.to_string(), -// amount: vec![coin(amount, denom)], -// } -// .into(); - -// let default_msg = token::Message { -// recipient: user_remote.as_bytes().to_vec().into(), -// amount: Uint256::from_u128(amount), -// metadata: Binary::default(), -// }; - -// for (mode, sender, origin, origin_sender, token_msg, expected_resp) in [ -// // happy -// ( -// TokenMode::Bridged, -// &mailbox, -// origin_domain, -// &router, -// default_msg.clone(), -// Ok(vec![mint_msg, send_msg.clone()]), -// ), -// ( -// TokenMode::Collateral, -// &mailbox, -// origin_domain, -// &router, -// default_msg.clone(), -// Ok(vec![send_msg]), -// ), -// // errors -// ( -// TokenMode::Bridged, -// &errortic, -// origin_domain, -// &router, -// default_msg.clone(), -// Err(ContractError::Unauthorized), -// ), -// ( -// TokenMode::Bridged, -// &mailbox, -// origin_domain, -// &errortic, -// default_msg.clone(), -// Err(ContractError::Unauthorized), -// ), -// ( -// TokenMode::Collateral, -// &errortic, -// origin_domain, -// &router, -// default_msg.clone(), -// Err(ContractError::Unauthorized), -// ), -// ( -// TokenMode::Collateral, -// &mailbox, -// origin_domain, -// &errortic, -// default_msg, -// Err(ContractError::Unauthorized), -// ), -// ] { -// let mut warp = TokenNative { -// env: env.clone(), -// ..Default::default() -// }; - -// warp.init_hack(&deployer, &owner, &mailbox, hrp, denom, mode)?; -// warp.router_enroll(&owner, origin_domain, router.as_bytes().into())?; - -// let resp = warp.mailbox_handle( -// sender, -// mailbox::HandleMsg { -// origin, -// sender: origin_sender.as_bytes().to_vec().into(), -// body: token_msg.into(), -// }, -// ); - -// assert_eq!( -// resp.map(|v| v.messages.into_iter().map(|v| v.msg).collect::>()), -// expected_resp -// ); -// } - -// Ok(()) -// } diff --git a/contracts/warp/native/src/tests/mod.rs b/contracts/warp/native/src/tests/mod.rs deleted file mode 100644 index e3c7904a..00000000 --- a/contracts/warp/native/src/tests/mod.rs +++ /dev/null @@ -1,132 +0,0 @@ -// use cosmwasm_std::{ -// from_binary, -// testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage}, -// Addr, Binary, Coin, Empty, Env, MessageInfo, OwnedDeps, Response, -// }; -// use hpl_interface::{ -// mailbox, router, -// token::TokenMode, -// token_native::{ExecuteMsg, QueryMsg}, -// }; -// use serde::de::DeserializeOwned; - -// use crate::{ -// contract::{execute, instantiate, query}, -// error::ContractError, -// msg::{InstantiateMsg, Metadata}, -// state::{HRP, MAILBOX, MODE, OWNER, TOKEN}, -// }; - -// mod contracts; - -// pub struct TokenNative { -// pub deps: OwnedDeps, -// pub env: Env, -// } - -// impl Default for TokenNative { -// fn default() -> Self { -// Self { -// deps: mock_dependencies(), -// env: mock_env(), -// } -// } -// } - -// impl TokenNative { -// #[allow(clippy::too_many_arguments)] -// pub fn init( -// &mut self, -// sender: &Addr, -// hrp: &str, -// owner: &Addr, -// mailbox: &Addr, -// denom: &str, -// metadata: Option, -// mode: TokenMode, -// ) -> Result { -// instantiate( -// self.deps.as_mut(), -// self.env.clone(), -// mock_info(sender.as_str(), &[]), -// InstantiateMsg { -// denom: denom.to_string(), -// metadata, -// mode, -// hrp: hrp.to_string(), -// owner: owner.to_string(), -// mailbox: mailbox.to_string(), -// }, -// ) -// } - -// pub fn init_hack( -// &mut self, -// _sender: &Addr, -// owner: &Addr, -// mailbox: &Addr, -// hrp: &str, -// denom: &str, -// mode: TokenMode, -// ) -> anyhow::Result<()> { -// MODE.save(&mut self.deps.storage, &mode)?; -// HRP.save(&mut self.deps.storage, &hrp.to_string())?; -// TOKEN.save(&mut self.deps.storage, &denom.to_string())?; -// OWNER.save(&mut self.deps.storage, owner)?; -// MAILBOX.save(&mut self.deps.storage, mailbox)?; - -// Ok(()) -// } - -// fn execute(&mut self, info: MessageInfo, msg: ExecuteMsg) -> Result { -// execute(self.deps.as_mut(), self.env.clone(), info, msg) -// } - -// #[allow(dead_code)] -// fn query(&self, msg: QueryMsg) -> Result { -// query(self.deps.as_ref(), self.env.clone(), msg) -// .map(|v| from_binary::(&v))? -// .map_err(|e| e.into()) -// } - -// pub fn router_enroll( -// &mut self, -// sender: &Addr, -// domain: u32, -// router: Binary, -// ) -> Result { -// self.execute( -// mock_info(sender.as_str(), &[]), -// ExecuteMsg::Router(router::RouterMsg::EnrollRemoteRouter { -// set: router::RouterSet { domain, router }, -// }), -// ) -// } - -// pub fn mailbox_handle( -// &mut self, -// sender: &Addr, -// handle_msg: mailbox::HandleMsg, -// ) -> Result { -// self.execute( -// mock_info(sender.as_str(), &[]), -// ExecuteMsg::Handle(handle_msg), -// ) -// } - -// pub fn transfer_remote( -// &mut self, -// sender: &Addr, -// fund: Coin, -// dest_domain: u32, -// recipient: Binary, -// ) -> Result { -// self.execute( -// mock_info(sender.as_str(), &[fund]), -// ExecuteMsg::TransferRemote { -// dest_domain, -// recipient, -// }, -// ) -// } -// } diff --git a/integration-test/Cargo.toml b/integration-test/Cargo.toml index 9e9ed859..63f76ff3 100644 --- a/integration-test/Cargo.toml +++ b/integration-test/Cargo.toml @@ -39,6 +39,9 @@ ripemd.workspace = true hex-literal.workspace = true ibcx-test-utils.workspace = true +rstest.workspace = true +cw20.workspace = true + hpl-ownable.workspace = true hpl-ism-multisig.workspace = true hpl-interface.workspace = true diff --git a/integration-test/tests/contracts/cw/deploy.rs b/integration-test/tests/contracts/cw/deploy.rs index b9a03c0e..5bb262de 100644 --- a/integration-test/tests/contracts/cw/deploy.rs +++ b/integration-test/tests/contracts/cw/deploy.rs @@ -5,33 +5,32 @@ use cosmwasm_std::HexBinary; use hpl_interface::{ core::mailbox, router::{DomainRouteSet, RouterMsg}, - warp::{self, cw20::Cw20ModeBridged}, + warp::{self, cw20::Cw20ModeBridged, native::NativeModeBriged}, }; -use test_tube::{Account, Runner, SigningAccount, Wasm}; +use osmosis_test_tube::osmosis_std::types::cosmwasm::wasm::v1::MsgInstantiateContractResponse; +use test_tube::{Account, ExecuteResponse, Runner, SigningAccount, Wasm}; use super::{ types::{Codes, CoreDeployments}, Hook, Ism, }; -fn instantiate<'a, M: Serialize, R: Runner<'a>>( +pub fn instantiate<'a, M: Serialize, R: Runner<'a>>( wasm: &Wasm<'a, R>, code: u64, deployer: &SigningAccount, name: &str, msg: &M, -) -> eyre::Result { - Ok(wasm - .instantiate( - code, - msg, - Some(&deployer.address()), - Some(name), - &[], - deployer, - )? - .data - .address) +) -> ExecuteResponse { + wasm.instantiate( + code, + msg, + Some(&deployer.address()), + Some(name), + &[], + deployer, + ) + .unwrap() } #[allow(clippy::too_many_arguments)] @@ -57,11 +56,13 @@ pub fn deploy_core<'a, R: Runner<'a>>( owner: deployer.address(), domain: origin_domain, }, - )?; + ) + .data + .address; // set default ism, hook, igp - let default_ism = default_ism.deploy(wasm, codes, deployer)?; + let default_ism = default_ism.deploy(wasm, codes, owner, deployer)?; let default_hook = default_hook.deploy(wasm, codes, mailbox.clone(), owner, deployer)?; let required_hook = required_hook.deploy(wasm, codes, mailbox.clone(), owner, deployer)?; @@ -106,7 +107,9 @@ pub fn deploy_core<'a, R: Runner<'a>>( &ReceiverInitMsg { hrp: hrp.to_string(), }, - )?; + ) + .data + .address; Ok(CoreDeployments { mailbox, @@ -126,29 +129,48 @@ pub fn deploy_warp_route_bridged<'a, R: Runner<'a>>( hrp: &str, codes: &Codes, denom: String, -) -> eyre::Result { - instantiate( - wasm, - codes.warp_cw20, - deployer, - "warp-cw20", - &warp::cw20::InstantiateMsg { - token: warp::TokenModeMsg::Bridged(Cw20ModeBridged { - code_id: codes.cw20_base, - init_msg: Box::new(warp::cw20::Cw20InitMsg { - name: denom.clone(), - symbol: denom, - decimals: 6, - initial_balances: vec![], - mint: None, - marketing: None, + token_type: warp::TokenType, +) -> ExecuteResponse { + match token_type { + warp::TokenType::Native(_) => instantiate( + wasm, + codes.warp_native, + deployer, + "warp-native", + &warp::native::InstantiateMsg { + token: warp::TokenModeMsg::Bridged(NativeModeBriged { + denom, + metadata: None, }), - }), - hrp: hrp.to_string(), - owner: owner.address(), - mailbox: mailbox.to_string(), - }, - ) + hrp: hrp.to_string(), + owner: owner.address(), + mailbox: mailbox.to_string(), + }, + ), + warp::TokenType::CW20 { .. } => instantiate( + wasm, + codes.warp_cw20, + deployer, + "warp-cw20", + &warp::cw20::InstantiateMsg { + token: warp::TokenModeMsg::Bridged(Cw20ModeBridged { + code_id: codes.cw20_base, + init_msg: Box::new(warp::cw20::Cw20InitMsg { + name: denom.clone(), + symbol: denom, + decimals: 6, + initial_balances: vec![], + mint: None, + marketing: None, + }), + }), + hrp: hrp.to_string(), + owner: owner.address(), + mailbox: mailbox.to_string(), + }, + ), + warp::TokenType::CW721 { .. } => todo!(), + } } #[allow(dead_code)] @@ -160,10 +182,10 @@ pub fn deploy_warp_route_collateral<'a, R: Runner<'a>>( hrp: &str, codes: &Codes, denom: String, -) -> eyre::Result { +) -> ExecuteResponse { if denom.starts_with(format!("{hrp}1").as_str()) { // cw20 - let route = instantiate( + instantiate( wasm, codes.warp_cw20, deployer, @@ -176,12 +198,10 @@ pub fn deploy_warp_route_collateral<'a, R: Runner<'a>>( owner: owner.address(), mailbox: mailbox.to_string(), }, - )?; - - Ok(route) + ) } else { // native - let route = instantiate( + instantiate( wasm, codes.warp_native, deployer, @@ -192,9 +212,7 @@ pub fn deploy_warp_route_collateral<'a, R: Runner<'a>>( owner: owner.address(), mailbox: mailbox.to_string(), }, - )?; - - Ok(route) + ) } } diff --git a/integration-test/tests/contracts/cw/hook.rs b/integration-test/tests/contracts/cw/hook.rs index 863dfda7..c7d4cfb3 100644 --- a/integration-test/tests/contracts/cw/hook.rs +++ b/integration-test/tests/contracts/cw/hook.rs @@ -10,7 +10,7 @@ use ibcx_test_utils::addr; use osmosis_test_tube::Wasm; use test_tube::{Account, Runner, SigningAccount}; -use super::{igp::Igp, types::Codes}; +use super::{igp::Igp, instantiate, types::Codes}; #[allow(dead_code)] pub enum Hook { @@ -43,8 +43,8 @@ pub enum Hook { }, } +#[allow(dead_code)] impl Hook { - #[allow(dead_code)] pub fn mock(gas: Uint256) -> Self { Self::Mock { gas } } @@ -183,6 +183,36 @@ impl Hook { Ok(hook) } + fn deploy_aggregate<'a, R: Runner<'a>>( + wasm: &Wasm<'a, R>, + code: u64, + codes: &Codes, + mailbox: String, + hooks: Vec, + owner: &SigningAccount, + deployer: &SigningAccount, + ) -> eyre::Result { + use hpl_interface::hook::aggregate::*; + + let hook_addrs = hooks + .into_iter() + .map(|hook| hook.deploy(wasm, codes, mailbox.clone(), owner, deployer)) + .collect::>>()?; + + let hook = instantiate( + wasm, + code, + deployer, + "cw-hpl-hook-aggregate", + &InstantiateMsg { + owner: owner.address(), + hooks: hook_addrs, + }, + ); + + Ok(hook.data.address) + } + pub fn deploy<'a, R: Runner<'a>>( self, wasm: &Wasm<'a, R>, @@ -193,7 +223,7 @@ impl Hook { ) -> eyre::Result { match self { Hook::Mock { gas } => Self::deploy_mock(wasm, codes, gas, deployer), - Hook::Igp(igp) => Ok(igp.deploy(wasm, codes, mailbox, owner, deployer)?.core), + Hook::Igp(igp) => Ok(igp.deploy(wasm, codes, owner, deployer)?.core), Hook::Merkle {} => Self::deploy_merkle(wasm, codes, mailbox, owner, deployer), Hook::Pausable {} => Self::deploy_pausable(wasm, codes, owner, deployer), Hook::Routing { routes } => Self::deploy_routing( @@ -266,16 +296,15 @@ impl Hook { Ok(hook_addr) } - Hook::Aggregate { .. } => todo!(), + Hook::Aggregate { hooks } => Self::deploy_aggregate( + wasm, + codes.hook_aggregate, + codes, + mailbox, + hooks, + owner, + deployer, + ), } } } - -pub fn prepare_routing_hook(routes: Vec<(u32, u128)>) -> Hook { - let routes = routes - .into_iter() - .map(|(domain, _)| (domain, Hook::Merkle {})) - .collect(); - - Hook::routing(routes) -} diff --git a/integration-test/tests/contracts/cw/igp.rs b/integration-test/tests/contracts/cw/igp.rs index 7479922c..62447f27 100644 --- a/integration-test/tests/contracts/cw/igp.rs +++ b/integration-test/tests/contracts/cw/igp.rs @@ -25,7 +25,6 @@ impl Igp { self, wasm: &Wasm<'a, R>, codes: &Codes, - mailbox: String, owner: &SigningAccount, deployer: &SigningAccount, ) -> eyre::Result { @@ -35,9 +34,9 @@ impl Igp { &igp::core::InstantiateMsg { hrp: self.hrp, owner: owner.address(), - mailbox, gas_token: self.gas_token, beneficiary: self.beneficiary, + default_gas_usage: 25_000, }, Some(deployer.address().as_str()), Some("cw-hpl-igp"), diff --git a/integration-test/tests/contracts/cw/ism.rs b/integration-test/tests/contracts/cw/ism.rs index 302dd18a..14d4a5ca 100644 --- a/integration-test/tests/contracts/cw/ism.rs +++ b/integration-test/tests/contracts/cw/ism.rs @@ -12,10 +12,14 @@ pub enum Ism { Routing(Vec<(u32, Self)>), Multisig { - hrp: String, validators: validator::TestValidators, }, + Aggregate { + isms: Vec, + threshold: u8, + }, + #[allow(dead_code)] Mock, } @@ -25,11 +29,8 @@ impl Ism { Self::Routing(isms) } - pub fn multisig(hrp: &str, validators: validator::TestValidators) -> Self { - Self::Multisig { - hrp: hrp.to_string(), - validators, - } + pub fn multisig(validators: validator::TestValidators) -> Self { + Self::Multisig { validators } } } @@ -48,16 +49,15 @@ impl Ism { fn deploy_multisig<'a, R: Runner<'a>>( wasm: &Wasm<'a, R>, codes: &Codes, - hrp: String, set: validator::TestValidators, + owner: &SigningAccount, deployer: &SigningAccount, ) -> eyre::Result { let multisig_ism = wasm .instantiate( codes.ism_multisig, &hpl_interface::ism::multisig::InstantiateMsg { - hrp: hrp.to_string(), - owner: deployer.address(), + owner: owner.address(), }, None, None, @@ -69,11 +69,9 @@ impl Ism { wasm.execute( &multisig_ism, - &hpl_interface::ism::multisig::ExecuteMsg::EnrollValidators { - set: set.to_set(&hrp), - }, + &hpl_interface::ism::multisig::ExecuteMsg::EnrollValidators { set: set.to_set() }, &[], - deployer, + owner, )?; wasm.execute( @@ -85,7 +83,7 @@ impl Ism { }, }, &[], - deployer, + owner, )?; Ok(multisig_ism) @@ -95,19 +93,20 @@ impl Ism { wasm: &Wasm<'a, R>, codes: &Codes, isms: Vec<(u32, Self)>, + owner: &SigningAccount, deployer: &SigningAccount, ) -> eyre::Result { let routing_ism = wasm .instantiate( codes.ism_routing, &hpl_interface::ism::routing::InstantiateMsg { - owner: deployer.address(), + owner: owner.address(), isms: isms .into_iter() .map(|(domain, ism)| { Ok(hpl_interface::ism::routing::IsmSet { domain, - address: ism.deploy(wasm, codes, deployer)?, + address: ism.deploy(wasm, codes, owner, deployer)?, }) }) .collect::>>()?, @@ -123,28 +122,71 @@ impl Ism { Ok(routing_ism) } + fn deploy_aggregate<'a, R: Runner<'a>>( + wasm: &Wasm<'a, R>, + codes: &Codes, + isms: Vec, + threshold: u8, + owner: &SigningAccount, + deployer: &SigningAccount, + ) -> eyre::Result { + use hpl_interface::ism::aggregate::*; + + let ism_addrs = isms + .into_iter() + .map(|v| v.deploy(wasm, codes, owner, deployer)) + .collect::>>()?; + + let aggregate_ism = wasm + .instantiate( + codes.ism_aggregate, + &InstantiateMsg { + owner: owner.address(), + isms: ism_addrs, + threshold, + }, + None, + None, + &[], + deployer, + )? + .data + .address; + + Ok(aggregate_ism) + } + pub fn deploy<'a, R: Runner<'a>>( self, wasm: &Wasm<'a, R>, codes: &Codes, + owner: &SigningAccount, deployer: &SigningAccount, ) -> eyre::Result { match self { Self::Mock => Self::deploy_mock(wasm, codes, deployer), - Self::Multisig { - hrp, - validators: set, - } => Self::deploy_multisig(wasm, codes, hrp, set, deployer), - Self::Routing(isms) => Self::deploy_routing(wasm, codes, isms, deployer), + Self::Multisig { validators: set } => { + Self::deploy_multisig(wasm, codes, set, owner, deployer) + } + Self::Aggregate { isms, threshold } => { + Self::deploy_aggregate(wasm, codes, isms, threshold, owner, deployer) + } + Self::Routing(isms) => Self::deploy_routing(wasm, codes, isms, owner, deployer), } } } -pub fn prepare_routing_ism(info: Vec<(u32, &str, TestValidators)>) -> Ism { +pub fn prepare_routing_ism(info: Vec<(u32, TestValidators)>) -> Ism { let mut isms = vec![]; - for (domain, hrp, set) in info { - isms.push((domain, Ism::multisig(hrp, set))); + for (domain, set) in info { + isms.push(( + domain, + Ism::Aggregate { + isms: vec![Ism::multisig(set)], + threshold: 1, + }, + )); } Ism::routing(isms) diff --git a/integration-test/tests/contracts/cw/mod.rs b/integration-test/tests/contracts/cw/mod.rs index 28a25936..c0762aee 100644 --- a/integration-test/tests/contracts/cw/mod.rs +++ b/integration-test/tests/contracts/cw/mod.rs @@ -6,8 +6,8 @@ mod setup; mod store; mod types; -pub use deploy::deploy_core; -pub use hook::{prepare_routing_hook, Hook}; +pub use deploy::*; +pub use hook::Hook; pub use ism::{prepare_routing_ism, Ism}; pub use setup::{setup_env, Env}; pub use store::store_code; diff --git a/integration-test/tests/contracts/cw/setup.rs b/integration-test/tests/contracts/cw/setup.rs index fafa83af..aa40b9dc 100644 --- a/integration-test/tests/contracts/cw/setup.rs +++ b/integration-test/tests/contracts/cw/setup.rs @@ -1,14 +1,17 @@ use std::{collections::BTreeMap, path::PathBuf}; -use cosmwasm_std::{coin, Coin}; +use cosmwasm_std::{coin, Coin, Uint256}; use hpl_interface::igp::oracle::RemoteGasDataConfig; use test_tube::{Account, Module, Runner, SigningAccount, Wasm}; use crate::validator::TestValidators; use super::{ - deploy_core, igp::Igp, prepare_routing_hook, prepare_routing_ism, store_code, - types::CoreDeployments, Hook, + deploy_core, + igp::Igp, + prepare_routing_ism, store_code, + types::{Codes, CoreDeployments}, + Hook, }; const DEFAULT_GAS: u128 = 300_000; @@ -18,6 +21,7 @@ pub struct Env<'a, R: Runner<'a>> { pub app: &'a R, pub core: CoreDeployments, + pub codes: Codes, pub domain: u32, acc_gen: Box SigningAccount>, @@ -52,20 +56,22 @@ pub fn setup_env<'a, R: Runner<'a>>( let deployer = acc_gen(app, &[coin(1_000_000u128.pow(3), "uosmo")]); let tester = acc_gen(app, &[coin(1_000_000u128.pow(3), "uosmo")]); - let default_ism = prepare_routing_ism( - validators - .iter() - .map(|v| (v.domain, hrp, v.clone())) - .collect(), - ); - let default_hook = - prepare_routing_hook(validators.iter().map(|v| (v.domain, DEFAULT_GAS)).collect()); - let required_hook = Hook::Igp(Igp { - hrp: hrp.to_string(), - gas_token: "uosmo".to_string(), - beneficiary: deployer.address(), - oracle_configs: oracle_config.to_vec(), - }); + let default_ism = + prepare_routing_ism(validators.iter().map(|v| (v.domain, v.clone())).collect()); + + let default_hook = Hook::mock(Uint256::from_u128(DEFAULT_GAS)); + + let required_hook = Hook::Aggregate { + hooks: vec![ + Hook::Merkle {}, + Hook::Igp(Igp { + hrp: hrp.to_string(), + gas_token: "uosmo".to_string(), + beneficiary: deployer.address(), + oracle_configs: oracle_config.to_vec(), + }), + ], + }; let wasm = Wasm::new(app); let codes = store_code(&wasm, &deployer, artifacts)?; @@ -86,6 +92,7 @@ pub fn setup_env<'a, R: Runner<'a>>( app, core, + codes, domain, acc_gen: Box::new(acc_gen), diff --git a/integration-test/tests/contracts/cw/store.rs b/integration-test/tests/contracts/cw/store.rs index 0e3a69ea..9db25a4d 100644 --- a/integration-test/tests/contracts/cw/store.rs +++ b/integration-test/tests/contracts/cw/store.rs @@ -7,9 +7,10 @@ use super::types::{Codes, CodesMap}; const DEFAULT_ARTIFACTS_PATH: &str = "../target/wasm32-unknown-unknown/release/"; -const CONTRACTS: [&str; 16] = [ +const CONTRACTS: [&str; 18] = [ "mailbox", "validator_announce", + "hook_aggregate", "hook_merkle", "hook_pausable", "hook_routing", @@ -17,6 +18,7 @@ const CONTRACTS: [&str; 16] = [ "hook_routing_fallback", "igp", "igp_oracle", + "ism_aggregate", "ism_multisig", "ism_routing", "test_mock_hook", diff --git a/integration-test/tests/contracts/cw/types.rs b/integration-test/tests/contracts/cw/types.rs index 41ec7468..d1f58024 100644 --- a/integration-test/tests/contracts/cw/types.rs +++ b/integration-test/tests/contracts/cw/types.rs @@ -17,6 +17,7 @@ pub struct Codes { #[serde(rename = "validator_announce")] pub va: u64, + pub hook_aggregate: u64, pub hook_merkle: u64, pub hook_pausable: u64, pub hook_routing: u64, @@ -26,6 +27,7 @@ pub struct Codes { pub igp: u64, pub igp_oracle: u64, + pub ism_aggregate: u64, pub ism_multisig: u64, pub ism_routing: u64, diff --git a/integration-test/tests/mailbox.rs b/integration-test/tests/mailbox.rs index 69ca8807..26aeef0a 100644 --- a/integration-test/tests/mailbox.rs +++ b/integration-test/tests/mailbox.rs @@ -8,14 +8,18 @@ use cosmwasm_std::{attr, coin, Attribute, Binary, Uint128}; use ethers::{ prelude::parse_log, providers::Middleware, signers::Signer, types::TransactionReceipt, }; -use osmosis_test_tube::{Account, Module, OsmosisTestApp, Wasm}; +use ibcx_test_utils::addr; +use osmosis_test_tube::{ + osmosis_std::types::cosmwasm::wasm::v1::MsgExecuteContractResponse, Account, Module, + OsmosisTestApp, Wasm, +}; use hpl_interface::{ core::mailbox::{self, DispatchMsg}, igp::oracle::RemoteGasDataConfig, - types::{bech32_decode, bech32_encode, bech32_to_h256}, + types::{bech32_decode, bech32_encode, bech32_to_h256, AggregateMetadata}, }; -use test_tube::Runner; +use test_tube::{ExecuteResponse, Runner}; use crate::{ constants::*, @@ -35,9 +39,9 @@ fn sorted(mut attrs: Vec) -> Vec { attrs } -async fn send_msg<'a, M, S, R>( - anvil: ð::Env, - cosmos: &cw::Env<'a, R>, +async fn send_msg_cw_to_evm<'a, M, S, R>( + from: &cw::Env<'a, R>, + to: ð::Env, ) -> eyre::Result where M: Middleware + 'static, @@ -45,13 +49,13 @@ where R: Runner<'a>, { let mut receiver = [0u8; 32]; - receiver[12..].copy_from_slice(&anvil.core.msg_receiver.address().0); - let _sender = bech32_decode(cosmos.acc_tester.address().as_str())?; + receiver[12..].copy_from_slice(&to.core.msg_receiver.address().0); + let _sender = bech32_decode(from.acc_tester.address().as_str())?; let msg_body = b"hello world"; // dispatch - let dispatch_res = Wasm::new(cosmos.app).execute( - &cosmos.core.mailbox, + let dispatch_res = Wasm::new(from.app).execute( + &from.core.mailbox, &mailbox::ExecuteMsg::Dispatch(DispatchMsg { dest_domain: DOMAIN_EVM, recipient_addr: receiver.into(), @@ -60,13 +64,13 @@ where metadata: None, }), &[coin(56_000_000, "uosmo")], - &cosmos.acc_tester, + &from.acc_tester, )?; let dispatch = parse_dispatch_from_res(&dispatch_res.events); let _dispatch_id = parse_dispatch_id_from_res(&dispatch_res.events); - let process_tx = anvil + let process_tx = to .core .mailbox .process(vec![].into(), Binary::from(dispatch.message).0.into()); @@ -75,6 +79,79 @@ where Ok(process_tx_res) } +async fn send_msg_evm_to_cw<'a, M, S, R>( + from: ð::Env, + to: &cw::Env<'a, R>, +) -> eyre::Result> +where + M: Middleware + 'static, + S: Signer + 'static, + R: Runner<'a>, +{ + // prepare message arguments + let sender = bech32_encode("osmo", from.acc_owner.address().as_bytes())?; + let receiver = bech32_to_h256(&to.core.msg_receiver)?; + let msg_body = b"hello world"; + + // dispatch + let dispatch_tx_call = from + .core + .mailbox + .dispatch(DOMAIN_OSMO, receiver, msg_body.into()); + let dispatch_res = dispatch_tx_call.send().await?.await?.unwrap(); + + let dispatch: DispatchFilter = parse_log(dispatch_res.logs[0].clone())?; + let dispatch_id: DispatchIdFilter = parse_log(dispatch_res.logs[1].clone())?; + + // generate ism metadata + let multisig_ism_metadata = to.get_validator_set(DOMAIN_EVM)?.make_metadata( + from.core.mailbox.address(), + from.core.mailbox.root().await?, + from.core.mailbox.count().await? - 1, + dispatch_id.message_id, + true, + )?; + + let aggregate_ism_metadata = AggregateMetadata::new(vec![( + addr(&to.core.default_ism), + multisig_ism_metadata.into(), + )]); + + // process + let process_res = Wasm::new(to.app).execute( + &to.core.mailbox, + &mailbox::ExecuteMsg::Process { + metadata: aggregate_ism_metadata.into(), + message: dispatch.message.to_vec().into(), + }, + &[], + &to.acc_owner, + )?; + let process_recv_evt = process_res + .events + .iter() + .find(|v| v.ty == "wasm-mailbox_msg_received") + .unwrap(); + + assert_eq!( + process_recv_evt.attributes, + sorted(vec![ + Attribute { + key: "_contract_address".to_string(), + value: to.core.msg_receiver.clone(), + }, + attr("sender", sender), + attr( + "origin", + from.core.mailbox.local_domain().await?.to_string() + ), + attr("body", std::str::from_utf8(msg_body)?), + ]), + ); + + Ok(process_res) +} + #[tokio::test] async fn test_mailbox_cw_to_evm() -> eyre::Result<()> { // init Osmosis env @@ -96,7 +173,7 @@ async fn test_mailbox_cw_to_evm() -> eyre::Result<()> { // init Anvil env let anvil1 = eth::setup_env(DOMAIN_EVM).await?; - let _ = send_msg(&anvil1, &osmo).await?; + let _ = send_msg_cw_to_evm(&osmo, &anvil1).await?; Ok(()) } @@ -122,61 +199,7 @@ async fn test_mailbox_evm_to_cw() -> eyre::Result<()> { // init Anvil env let anvil1 = eth::setup_env(DOMAIN_EVM).await?; - // prepare message arguments - let sender = bech32_encode("osmo", anvil1.acc_owner.address().as_bytes())?; - let receiver = bech32_to_h256(&osmo.core.msg_receiver)?; - let msg_body = b"hello world"; - - // dispatch - let dispatch_tx_call = anvil1 - .core - .mailbox - .dispatch(DOMAIN_OSMO, receiver, msg_body.into()); - let dispatch_res = dispatch_tx_call.send().await?.await?.unwrap(); - - let dispatch: DispatchFilter = parse_log(dispatch_res.logs[0].clone())?; - let dispatch_id: DispatchIdFilter = parse_log(dispatch_res.logs[1].clone())?; - - // generate ism metadata - let ism_metadata = osmo.get_validator_set(DOMAIN_EVM)?.make_metadata( - anvil1.core.mailbox.address(), - anvil1.core.mailbox.root().await?, - anvil1.core.mailbox.count().await?, - dispatch_id.message_id, - true, - )?; - - // process - let process_res = Wasm::new(osmo.app).execute( - &osmo.core.mailbox, - &mailbox::ExecuteMsg::Process { - metadata: ism_metadata.into(), - message: dispatch.message.to_vec().into(), - }, - &[], - &osmo.acc_owner, - )?; - let process_recv_evt = process_res - .events - .iter() - .find(|v| v.ty == "wasm-mailbox_msg_received") - .unwrap(); - - assert_eq!( - process_recv_evt.attributes, - sorted(vec![ - Attribute { - key: "_contract_address".to_string(), - value: osmo.core.msg_receiver, - }, - attr("sender", sender), - attr( - "origin", - anvil1.core.mailbox.local_domain().await?.to_string() - ), - attr("body", std::str::from_utf8(msg_body)?), - ]), - ); + let _ = send_msg_evm_to_cw(&anvil1, &osmo).await?; Ok(()) } diff --git a/integration-test/tests/validator.rs b/integration-test/tests/validator.rs index 959f0fbd..05230e41 100644 --- a/integration-test/tests/validator.rs +++ b/integration-test/tests/validator.rs @@ -6,7 +6,9 @@ use ethers::types::{Address, H160}; use ethers::utils::hex::FromHex; use hpl_interface::{ ism::multisig::{ThresholdSet, ValidatorSet}, - types::{bech32_encode, pub_to_addr, Message, MessageIdMultisigIsmMetadata}, + types::{ + bech32_encode, eth_addr, eth_hash, pub_to_addr, Message, MessageIdMultisigIsmMetadata, + }, }; use ibcx_test_utils::{addr, gen_bz}; use k256::{ @@ -54,11 +56,10 @@ impl TestValidator { .into() } - pub fn to_val(&self, domain: u32, hrp: &str) -> ValidatorSet { + pub fn to_val(&self, domain: u32) -> ValidatorSet { ValidatorSet { domain, - validator: self.addr(hrp), - validator_pubkey: self.pub_key_to_binary(), + validator: eth_addr(self.pub_key.to_encoded_point(false).as_bytes().into()).unwrap(), } } @@ -108,10 +109,10 @@ impl TestValidators { } } - pub fn to_set(&self, hrp: &str) -> Vec { + pub fn to_set(&self) -> Vec { self.validators .iter() - .map(|v| v.to_val(self.domain, hrp)) + .map(|v| v.to_val(self.domain)) .collect::>() } @@ -123,7 +124,7 @@ impl TestValidators { .iter() .map(|v| { let (mut signature, recov_id) = v.sign(digest); - signature.0.extend(vec![recov_id.to_byte()]); + signature.0.extend(vec![recov_id.to_byte() + 27]); signature.into() }) .collect::>(); @@ -145,11 +146,11 @@ impl TestValidators { let multisig_hash = hpl_ism_multisig::multisig_hash( hpl_ism_multisig::domain_hash(self.domain, addr.to_vec().into())?.to_vec(), merkle_root.to_vec(), - merkle_index.to_be_bytes().to_vec(), + merkle_index, message_id.to_vec(), )?; - let hashed_message = hpl_ism_multisig::eth_hash(multisig_hash)?; + let hashed_message = eth_hash(multisig_hash)?; let signatures = if is_passed { self.sign(self.threshold, hashed_message.as_slice().try_into()?) @@ -168,7 +169,6 @@ impl TestValidators { #[test] fn test_validator() { - let hrp = "osmo"; let owner = addr("owner"); let validators = TestValidators::new(2, 5, 3); @@ -176,14 +176,10 @@ fn test_validator() { hpl_ownable::initialize(deps.as_mut().storage, &owner).unwrap(); - hpl_ism_multisig::state::HRP - .save(deps.as_mut().storage, &hrp.to_string()) - .unwrap(); - hpl_ism_multisig::execute::enroll_validators( deps.as_mut(), mock_info(owner.as_str(), &[]), - validators.to_set(hrp), + validators.to_set(), ) .unwrap(); @@ -206,7 +202,7 @@ fn test_validator() { .make_metadata( H160::from_slice(gen_bz(20).as_slice()), gen_bz(32).as_slice().try_into().unwrap(), - 1, + 0, message_id.as_slice().try_into().unwrap(), true, ) diff --git a/integration-test/tests/warp.rs b/integration-test/tests/warp.rs new file mode 100644 index 00000000..5821b530 --- /dev/null +++ b/integration-test/tests/warp.rs @@ -0,0 +1,288 @@ +#[allow(dead_code)] +mod constants; +mod contracts; +mod event; +mod validator; + +use std::collections::BTreeMap; + +use cosmwasm_std::{Event, Uint128}; +use hpl_interface::{igp::oracle::RemoteGasDataConfig, warp}; +use osmosis_test_tube::{ + osmosis_std::types::osmosis::tokenfactory::{ + self, + v1beta1::{MsgChangeAdmin, MsgCreateDenom}, + }, + OsmosisTestApp, TokenFactory, +}; +use rstest::rstest; +use test_tube::{Account, Module, Wasm}; + +use crate::{constants::*, contracts::cw, validator::TestValidators}; + +fn wasm_events(events: Vec) -> BTreeMap> { + events + .into_iter() + .filter(|v| v.ty.starts_with("wasm-")) + .map(|v| { + ( + v.ty, + v.attributes + .into_iter() + .map(|x| (x.key, x.value)) + .collect::>(), + ) + }) + .collect::>() +} + +#[tokio::test] +async fn test_cw20_colleteral() -> eyre::Result<()> { + let osmo_app = OsmosisTestApp::new(); + let osmo = cw::setup_env( + &osmo_app, + |app, coins| app.init_account(coins).unwrap(), + None::<&str>, + "osmo", + DOMAIN_OSMO, + &[TestValidators::new(DOMAIN_EVM, 5, 3)], + &[RemoteGasDataConfig { + remote_domain: DOMAIN_EVM, + token_exchange_rate: Uint128::from(10u128.pow(4)), + gas_price: Uint128::from(10u128.pow(9)), + }], + )?; + + let wasm = Wasm::new(&osmo_app); + + // deploy new cw20 token + let mock_token = cw::instantiate( + &wasm, + osmo.codes.cw20_base, + &osmo.acc_deployer, + "cw20-base", + &hpl_interface::warp::cw20::Cw20InitMsg { + name: "denomdenom".into(), + symbol: "denomdenom".into(), + decimals: 6, + initial_balances: vec![], + mint: Some(cw20::MinterResponse { + minter: osmo.acc_owner.address(), + cap: None, + }), + marketing: None, + }, + ) + .data + .address; + + // deploy warp route with deployed cw20 token + let warp_resp = cw::deploy_warp_route_collateral( + &wasm, + &osmo.acc_owner, + &osmo.acc_deployer, + &osmo.core.mailbox, + "osmo", + &osmo.codes, + mock_token.clone(), + ); + + let events = wasm_events(warp_resp.events); + assert_eq!( + events["wasm-hpl_warp_cw20::instantiate"]["denom"], + mock_token + ); + + // move cw20 token's minter to warp route contract + wasm.execute( + &mock_token, + &cw20::Cw20ExecuteMsg::UpdateMinter { + new_minter: Some(warp_resp.data.address), + }, + &[], + &osmo.acc_owner, + )?; + + // ready to mint / burn! + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn test_cw20_bridged() -> eyre::Result<()> { + let osmo_app = OsmosisTestApp::new(); + let osmo = cw::setup_env( + &osmo_app, + |app, coins| app.init_account(coins).unwrap(), + None::<&str>, + "osmo", + DOMAIN_OSMO, + &[TestValidators::new(DOMAIN_EVM, 5, 3)], + &[RemoteGasDataConfig { + remote_domain: DOMAIN_EVM, + token_exchange_rate: Uint128::from(10u128.pow(4)), + gas_price: Uint128::from(10u128.pow(9)), + }], + )?; + + let wasm = Wasm::new(&osmo_app); + + let warp_resp = cw::deploy_warp_route_bridged( + &wasm, + &osmo.acc_owner, + &osmo.acc_deployer, + &osmo.core.mailbox, + "osmo", + &osmo.codes, + "denomdenom".into(), + warp::TokenType::CW20 { + contract: "".into(), + }, + ); + + let events = wasm_events(warp_resp.events); + assert_eq!( + events["wasm-hpl_warp_cw20::instantiate"]["denom"], + "denomdenom" + ); + + let token_addr_from_evt = &events["wasm-hpl_warp_cw20::reply-init"]["new_token"]; + let token_info: cw20::TokenInfoResponse = + wasm.query(token_addr_from_evt, &cw20::Cw20QueryMsg::TokenInfo {})?; + assert_eq!(token_info.name, "denomdenom"); + + let minter_info: Option = + wasm.query(token_addr_from_evt, &cw20::Cw20QueryMsg::Minter {})?; + assert_eq!(minter_info.unwrap().minter, warp_resp.data.address); + + Ok(()) +} + +#[rstest] +#[case("utest")] +#[tokio::test] +async fn test_native_collateral(#[case] denom: &str) -> eyre::Result<()> { + let osmo_app = OsmosisTestApp::new(); + let osmo = cw::setup_env( + &osmo_app, + |app, coins| app.init_account(coins).unwrap(), + None::<&str>, + "osmo", + DOMAIN_OSMO, + &[TestValidators::new(DOMAIN_EVM, 5, 3)], + &[RemoteGasDataConfig { + remote_domain: DOMAIN_EVM, + token_exchange_rate: Uint128::from(10u128.pow(4)), + gas_price: Uint128::from(10u128.pow(9)), + }], + )?; + + let wasm = Wasm::new(&osmo_app); + let tf = TokenFactory::new(&osmo_app); + + let mock_token = tf + .create_denom( + MsgCreateDenom { + sender: osmo.acc_deployer.address(), + subdenom: denom.to_string(), + }, + &osmo.acc_deployer, + )? + .data + .new_token_denom; + + let warp_resp = cw::deploy_warp_route_collateral( + &wasm, + &osmo.acc_owner, + &osmo.acc_deployer, + &osmo.core.mailbox, + "osmo", + &osmo.codes, + mock_token.clone(), + ); + + let events = wasm_events(warp_resp.events); + assert_eq!( + events["wasm-hpl_warp_native::instantiate"]["denom"], + mock_token + ); + + let resp = tf.query_denom_authority_metadata( + &tokenfactory::v1beta1::QueryDenomAuthorityMetadataRequest { + denom: mock_token.clone(), + }, + )?; + assert_eq!( + resp.authority_metadata.unwrap().admin, + osmo.acc_deployer.address() + ); + + tf.change_admin( + MsgChangeAdmin { + sender: osmo.acc_deployer.address(), + denom: mock_token, + new_admin: warp_resp.data.address, + }, + &osmo.acc_deployer, + )?; + + // ready to test! + + Ok(()) +} + +#[rstest] +#[case("utest")] +#[tokio::test] +async fn test_native_bridged(#[case] denom: &str) -> eyre::Result<()> { + let osmo_app = OsmosisTestApp::new(); + let osmo = cw::setup_env( + &osmo_app, + |app, coins| app.init_account(coins).unwrap(), + None::<&str>, + "osmo", + DOMAIN_OSMO, + &[TestValidators::new(DOMAIN_EVM, 5, 3)], + &[RemoteGasDataConfig { + remote_domain: DOMAIN_EVM, + token_exchange_rate: Uint128::from(10u128.pow(4)), + gas_price: Uint128::from(10u128.pow(9)), + }], + )?; + + let wasm = Wasm::new(&osmo_app); + let tf = TokenFactory::new(&osmo_app); + + let warp_resp = cw::deploy_warp_route_bridged( + &wasm, + &osmo.acc_owner, + &osmo.acc_deployer, + &osmo.core.mailbox, + "osmo", + &osmo.codes, + denom.into(), + warp::TokenType::Native(warp::TokenTypeNative::Fungible { denom: "".into() }), + ); + + let events = wasm_events(warp_resp.events); + assert_eq!(events["wasm-hpl_warp_native::instantiate"]["denom"], denom); + + let new_denom = &events["wasm-hpl_warp_native::reply-init"]["denom"]; + assert_eq!( + new_denom.clone(), + format!("factory/{}/{denom}", warp_resp.data.address) + ); + + let resp = tf.query_denom_authority_metadata( + &tokenfactory::v1beta1::QueryDenomAuthorityMetadataRequest { + denom: new_denom.clone(), + }, + )?; + assert_eq!( + resp.authority_metadata.unwrap().admin, + warp_resp.data.address + ); + + Ok(()) +} diff --git a/packages/connection/Cargo.toml b/packages/connection/Cargo.toml new file mode 100644 index 00000000..4eaf5e0b --- /dev/null +++ b/packages/connection/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "hpl-connection" +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 +cw-storage-plus.workspace = true +cw2.workspace = true +bech32.workspace = true +sha3.workspace = true +schemars.workspace = true +serde.workspace = true +serde-json-wasm.workspace = true +thiserror.workspace = true +cosmwasm-schema.workspace = true + +hpl-ownable.workspace = true +hpl-interface.workspace = true + +[dev-dependencies] +anyhow.workspace = true diff --git a/packages/connection/src/lib.rs b/packages/connection/src/lib.rs new file mode 100644 index 00000000..5506c9d8 --- /dev/null +++ b/packages/connection/src/lib.rs @@ -0,0 +1,100 @@ +use cosmwasm_std::{ + ensure_eq, Addr, CustomQuery, Deps, DepsMut, Env, Event, MessageInfo, QueryResponse, Response, + StdError, StdResult, Storage, +}; +use cw_storage_plus::Item; +use hpl_interface::connection::{ + ConnectionMsg, ConnectionQueryMsg, HookResponse, IsmResponse, MailboxResponse, +}; + +const MAILBOX_KEY: &str = "conn::mailbox"; +const MAILBOX: Item = Item::new(MAILBOX_KEY); + +const ISM_KEY: &str = "conn::ism"; +const ISM: Item = Item::new(ISM_KEY); + +const HOOK_KEY: &str = "conn::hook"; +const HOOK: Item = Item::new(HOOK_KEY); + +fn event_to_resp(event: Event) -> Response { + Response::new().add_event(event) +} + +fn new_event(name: &str) -> Event { + Event::new(format!("hpl_connection::{}", name)) +} + +pub fn handle( + deps: DepsMut<'_, C>, + _env: Env, + info: MessageInfo, + msg: ConnectionMsg, +) -> StdResult { + use ConnectionMsg::*; + + ensure_eq!( + hpl_ownable::get_owner(deps.storage)?, + info.sender, + StdError::generic_err("unauthorized") + ); + + match msg { + SetMailbox { mailbox } => { + let mailbox_addr = deps.api.addr_validate(&mailbox)?; + + MAILBOX.save(deps.storage, &mailbox_addr)?; + + Ok(event_to_resp( + new_event("set_mailbox").add_attribute("mailbox", mailbox), + )) + } + SetIsm { ism } => { + let ism_addr = deps.api.addr_validate(&ism)?; + + ISM.save(deps.storage, &ism_addr)?; + + Ok(event_to_resp( + new_event("set_ism").add_attribute("ism", ism), + )) + } + SetHook { hook } => { + let hook_addr = deps.api.addr_validate(&hook)?; + + HOOK.save(deps.storage, &hook_addr)?; + + Ok(event_to_resp( + new_event("set_hook").add_attribute("hook", hook), + )) + } + } +} + +pub fn handle_query( + deps: Deps<'_, C>, + _env: Env, + msg: ConnectionQueryMsg, +) -> StdResult { + match msg { + ConnectionQueryMsg::GetMailbox {} => Ok(cosmwasm_std::to_binary(&MailboxResponse { + mailbox: get_mailbox(deps.storage)?.map(|v| v.into()), + })?), + ConnectionQueryMsg::GetHook {} => Ok(cosmwasm_std::to_binary(&HookResponse { + hook: get_hook(deps.storage)?.map(|v| v.into()), + })?), + ConnectionQueryMsg::GetIsm {} => Ok(cosmwasm_std::to_binary(&IsmResponse { + ism: get_ism(deps.storage)?.map(|v| v.into()), + })?), + } +} + +pub fn get_mailbox(storage: &dyn Storage) -> StdResult> { + MAILBOX.may_load(storage) +} + +pub fn get_ism(storage: &dyn Storage) -> StdResult> { + ISM.may_load(storage) +} + +pub fn get_hook(storage: &dyn Storage) -> StdResult> { + HOOK.may_load(storage) +} diff --git a/packages/interface/src/connection.rs b/packages/interface/src/connection.rs index db5fff04..7e655037 100644 --- a/packages/interface/src/connection.rs +++ b/packages/interface/src/connection.rs @@ -4,7 +4,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; pub enum ConnectionMsg { SetMailbox { mailbox: String }, - SetIgp { igp: String }, + SetHook { hook: String }, SetIsm { ism: String }, } @@ -15,8 +15,8 @@ pub enum ConnectionQueryMsg { #[returns(MailboxResponse)] GetMailbox {}, - #[returns(IgpResponse)] - GetIgp {}, + #[returns(HookResponse)] + GetHook {}, #[returns(IsmResponse)] GetIsm {}, @@ -24,15 +24,15 @@ pub enum ConnectionQueryMsg { #[cw_serde] pub struct MailboxResponse { - pub mailbox: String, + pub mailbox: Option, } #[cw_serde] -pub struct IgpResponse { - pub igp: String, +pub struct HookResponse { + pub hook: Option, } #[cw_serde] pub struct IsmResponse { - pub ism: String, + pub ism: Option, } diff --git a/packages/interface/src/core/mailbox.rs b/packages/interface/src/core/mailbox.rs index da5c1b77..b5aa8c7e 100644 --- a/packages/interface/src/core/mailbox.rs +++ b/packages/interface/src/core/mailbox.rs @@ -1,6 +1,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{wasm_execute, Addr, Api, CosmosMsg, HexBinary, StdResult}; +use cosmwasm_std::{wasm_execute, Addr, Api, Coin, CosmosMsg, HexBinary, StdResult}; +#[allow(unused_imports)] use crate::{ hook::QuoteDispatchResponse, ownable::{OwnableMsg, OwnableQueryMsg}, @@ -109,6 +110,7 @@ pub fn dispatch( msg_body: HexBinary, hook: Option, metadata: Option, + funds: Vec, ) -> StdResult { Ok(wasm_execute( mailbox, @@ -119,7 +121,7 @@ pub fn dispatch( hook, metadata, }), - vec![], + funds, )? .into()) } diff --git a/packages/interface/src/core/mod.rs b/packages/interface/src/core/mod.rs index 2aaf00f4..7dee669b 100644 --- a/packages/interface/src/core/mod.rs +++ b/packages/interface/src/core/mod.rs @@ -5,6 +5,7 @@ pub mod mailbox; pub mod va; #[cw_serde] +#[derive(Default)] pub struct HandleMsg { pub origin: u32, pub sender: HexBinary, diff --git a/packages/interface/src/core/va.rs b/packages/interface/src/core/va.rs index 7bf018ae..7a9ad92a 100644 --- a/packages/interface/src/core/va.rs +++ b/packages/interface/src/core/va.rs @@ -11,8 +11,8 @@ pub struct InstantiateMsg { pub enum ExecuteMsg { Announce { validator: HexBinary, - storage_location: String, signature: HexBinary, + storage_location: String, }, } diff --git a/packages/interface/src/hook/aggregate.rs b/packages/interface/src/hook/aggregate.rs new file mode 100644 index 00000000..0036cb46 --- /dev/null +++ b/packages/interface/src/hook/aggregate.rs @@ -0,0 +1,72 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; + +use crate::ownable::{OwnableMsg, OwnableQueryMsg}; + +use super::{HookQueryMsg, PostDispatchMsg}; + +pub const TREE_DEPTH: usize = 32; + +#[cw_serde] +pub struct InstantiateMsg { + pub owner: String, + pub hooks: Vec, +} + +#[cw_serde] +pub enum ExecuteMsg { + Ownable(OwnableMsg), + PostDispatch(PostDispatchMsg), + SetHooks { hooks: Vec }, +} + +#[cw_serde] +#[derive(QueryResponses)] +#[query_responses(nested)] +pub enum QueryMsg { + Ownable(OwnableQueryMsg), + Hook(HookQueryMsg), + AggregateHook(AggregateHookQueryMsg), +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum AggregateHookQueryMsg { + #[returns(HooksResponse)] + Hooks {}, +} + +#[cw_serde] +pub struct HooksResponse { + pub hooks: Vec, +} + +#[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/mod.rs b/packages/interface/src/hook/mod.rs index ec1580d4..9d2a036d 100644 --- a/packages/interface/src/hook/mod.rs +++ b/packages/interface/src/hook/mod.rs @@ -1,3 +1,4 @@ +pub mod aggregate; pub mod merkle; pub mod pausable; pub mod routing; diff --git a/packages/interface/src/igp/core.rs b/packages/interface/src/igp/core.rs index 66cdd48b..b71287cf 100644 --- a/packages/interface/src/igp/core.rs +++ b/packages/interface/src/igp/core.rs @@ -5,6 +5,7 @@ use crate::{ hook::{HookQueryMsg, PostDispatchMsg}, ownable::{OwnableMsg, OwnableQueryMsg}, router::{RouterMsg, RouterQuery}, + Order, }; use super::oracle::IgpGasOracleQueryMsg; @@ -13,9 +14,9 @@ use super::oracle::IgpGasOracleQueryMsg; pub struct InstantiateMsg { pub hrp: String, pub owner: String, - pub mailbox: String, pub gas_token: String, pub beneficiary: String, + pub default_gas_usage: u128, } #[cw_serde] @@ -50,6 +51,16 @@ pub enum ExecuteMsg { PostDispatch(PostDispatchMsg), // base + SetDefaultGas { + gas: u128, + }, + SetGasForDomain { + config: Vec<(u32, u128)>, + }, + UnsetGasForDomain { + domains: Vec, + }, + SetBeneficiary { beneficiary: String, }, @@ -79,6 +90,19 @@ pub enum QueryMsg { #[cw_serde] #[derive(QueryResponses)] pub enum IgpQueryMsg { + #[returns(DefaultGasResponse)] + DefaultGas {}, + + #[returns(GasForDomainResponse)] + GasForDomain { domains: Vec }, + + #[returns(GasForDomainResponse)] + ListGasForDomains { + offset: Option, + limit: Option, + order: Option, + }, + #[returns(BeneficiaryResponse)] Beneficiary {}, @@ -95,6 +119,16 @@ impl IgpQueryMsg { } } +#[cw_serde] +pub struct DefaultGasResponse { + pub gas: u128, +} + +#[cw_serde] +pub struct GasForDomainResponse { + pub gas: Vec<(u32, u128)>, +} + #[cw_serde] pub struct BeneficiaryResponse { pub beneficiary: String, diff --git a/packages/interface/src/ism/aggregate.rs b/packages/interface/src/ism/aggregate.rs new file mode 100644 index 00000000..fcb48d5c --- /dev/null +++ b/packages/interface/src/ism/aggregate.rs @@ -0,0 +1,68 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; + +use crate::ownable::{OwnableMsg, OwnableQueryMsg}; + +use super::IsmQueryMsg; + +#[cw_serde] +pub struct InstantiateMsg { + pub owner: String, + pub isms: Vec, + pub threshold: u8, +} + +#[cw_serde] +pub enum ExecuteMsg { + Ownable(OwnableMsg), + + SetIsms { isms: Vec }, +} + +#[cw_serde] +#[derive(QueryResponses)] +#[query_responses(nested)] +pub enum QueryMsg { + Ownable(OwnableQueryMsg), + + Ism(IsmQueryMsg), + + AggregateIsm(AggregateIsmQueryMsg), +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum AggregateIsmQueryMsg { + #[returns(IsmsResponse)] + Isms {}, +} + +#[cw_serde] +pub struct IsmsResponse { + pub isms: Vec, +} + +#[cfg(test)] +mod test { + use cosmwasm_std::HexBinary; + + use super::*; + use crate::{ism::IsmQueryMsg, msg_checker}; + + #[test] + fn test_ism_interface() { + let _checked: QueryMsg = msg_checker(IsmQueryMsg::ModuleType {}.wrap()); + let _checked: QueryMsg = msg_checker( + IsmQueryMsg::Verify { + metadata: HexBinary::default(), + message: HexBinary::default(), + } + .wrap(), + ); + let _checked: QueryMsg = msg_checker( + IsmQueryMsg::VerifyInfo { + message: HexBinary::default(), + } + .wrap(), + ); + } +} diff --git a/packages/interface/src/ism/mod.rs b/packages/interface/src/ism/mod.rs index 75fa3a8a..13cccbe4 100644 --- a/packages/interface/src/ism/mod.rs +++ b/packages/interface/src/ism/mod.rs @@ -1,3 +1,4 @@ +pub mod aggregate; pub mod multisig; pub mod routing; @@ -46,6 +47,13 @@ pub enum ExpectedIsmQueryMsg { Ism(IsmQueryMsg), } +#[cw_serde] +#[derive(QueryResponses)] +#[query_responses(nested)] +pub enum ExpectedIsmSpecifierQueryMsg { + IsmSpecifier(IsmSpecifierQueryMsg), +} + #[cw_serde] #[derive(QueryResponses)] pub enum IsmSpecifierQueryMsg { @@ -53,6 +61,12 @@ pub enum IsmSpecifierQueryMsg { InterchainSecurityModule(), } +impl IsmSpecifierQueryMsg { + pub fn wrap(self) -> ExpectedIsmSpecifierQueryMsg { + ExpectedIsmSpecifierQueryMsg::IsmSpecifier(self) + } +} + #[cw_serde] pub struct ModuleTypeResponse { #[serde(rename = "type")] @@ -67,7 +81,7 @@ pub struct VerifyResponse { #[cw_serde] pub struct VerifyInfoResponse { pub threshold: u8, - pub validators: Vec, + pub validators: Vec, } #[cw_serde] @@ -81,7 +95,7 @@ pub fn recipient( ) -> StdResult> { let res = querier.query_wasm_smart::( recipient, - &IsmSpecifierQueryMsg::InterchainSecurityModule(), + &IsmSpecifierQueryMsg::InterchainSecurityModule().wrap(), )?; Ok(res.ism) diff --git a/packages/interface/src/ism/multisig.rs b/packages/interface/src/ism/multisig.rs index 0727ea78..4f91ac67 100644 --- a/packages/interface/src/ism/multisig.rs +++ b/packages/interface/src/ism/multisig.rs @@ -10,14 +10,12 @@ use super::{ModuleTypeResponse, VerifyInfoResponse, VerifyResponse}; #[cw_serde] pub struct InstantiateMsg { pub owner: String, - pub hrp: String, } #[cw_serde] pub struct ValidatorSet { pub domain: u32, - pub validator: String, - pub validator_pubkey: HexBinary, + pub validator: HexBinary, } #[cw_serde] @@ -32,7 +30,7 @@ pub enum ExecuteMsg { EnrollValidator { set: ValidatorSet }, EnrollValidators { set: Vec }, - UnenrollValidator { domain: u32, validator: String }, + UnenrollValidator { domain: u32, validator: HexBinary }, SetThreshold { set: ThresholdSet }, SetThresholds { set: Vec }, @@ -56,7 +54,7 @@ pub enum MultisigIsmQueryMsg { #[cw_serde] pub struct EnrolledValidatorsResponse { - pub validators: Vec, + pub validators: Vec, pub threshold: u8, } diff --git a/packages/interface/src/types/bech32.rs b/packages/interface/src/types/bech32.rs index 4470eaee..b2441e8d 100644 --- a/packages/interface/src/types/bech32.rs +++ b/packages/interface/src/types/bech32.rs @@ -46,6 +46,7 @@ pub fn bech32_encode(hrp: &str, raw_addr: &[u8]) -> StdResult { #[cfg(test)] mod tests { + use cosmwasm_std::HexBinary; use rstest::rstest; use crate::types::bech32_to_h256; @@ -64,6 +65,24 @@ mod tests { ); } + #[test] + fn conv() { + let addr = "dual1dwnrgwsf5c9vqjxsax04pdm0mx007yrraj2dgn"; + let addr_bin = bech32_decode(addr).unwrap(); + let addr_hex = HexBinary::from(addr_bin).to_hex(); + + println!("{}", addr_hex); + } + + #[test] + fn conv_rev() { + let addr_hex = "f3aa0d652226e21ae35cd9035c492ae41725edc9036edf0d6a48701b153b90a0"; + let addr_bin = HexBinary::from_hex(addr_hex).unwrap(); + let addr = bech32_encode("dual", &addr_bin).unwrap(); + + println!("{}", addr); + } + #[rstest] #[case( "osmo", diff --git a/packages/interface/src/types/crypto.rs b/packages/interface/src/types/crypto.rs index 28f322a3..2bf1c759 100644 --- a/packages/interface/src/types/crypto.rs +++ b/packages/interface/src/types/crypto.rs @@ -20,6 +20,16 @@ pub fn eth_hash(message: HexBinary) -> StdResult { Ok(message_hash) } +pub fn eth_addr(pubkey: HexBinary) -> StdResult { + let hash = keccak256_hash(&pubkey.as_slice()[1..]); + + let mut bytes = [0u8; 20]; + bytes.copy_from_slice(&hash.as_slice()[12..]); + let addr = HexBinary::from(bytes.to_vec()); + + Ok(addr.to_vec().into()) +} + pub fn sha256_digest(bz: impl AsRef<[u8]>) -> StdResult<[u8; 32]> { use sha2::{Digest, Sha256}; diff --git a/packages/interface/src/types/metadata.rs b/packages/interface/src/types/metadata.rs index eb93c7ad..15b56ab1 100644 --- a/packages/interface/src/types/metadata.rs +++ b/packages/interface/src/types/metadata.rs @@ -115,23 +115,61 @@ impl From for MessageIdMultisigIsmMetadata { } } +impl MessageIdMultisigIsmMetadata { + pub fn merkle_index(&self) -> u32 { + u32::from_be_bytes(self.merkle_index.to_vec().try_into().unwrap()) + } +} + +use std::convert::AsMut; + +fn clone_into_array(slice: &[T]) -> A +where + A: Sized + Default + AsMut<[T]>, + T: Clone, +{ + let mut a = Default::default(); + >::as_mut(&mut a).clone_from_slice(slice); + a +} + #[cw_serde] -pub struct AggregateIsmMetadata(pub BTreeMap); +pub struct AggregateMetadata(BTreeMap); + +impl AggregateMetadata { + pub const RANGE_SIZE: usize = 4; -impl AggregateIsmMetadata { - const RANGE_SIZE: usize = 4; + pub fn new(set: Vec<(Addr, HexBinary)>) -> Self { + Self(set.into_iter().collect()) + } +} - pub fn from_hex(v: HexBinary, isms: Vec) -> Self { +impl Iterator for AggregateMetadata { + type Item = (Addr, HexBinary); + + fn next(&mut self) -> Option { + self.0.pop_first() + } +} + +impl AggregateMetadata { + pub fn from_hex(v: HexBinary, addrs: Vec) -> Self { Self( - isms.into_iter() + addrs + .into_iter() .enumerate() .map(|(i, ism)| { let start = i * Self::RANGE_SIZE * 2; let mid = start + Self::RANGE_SIZE; let end = mid + Self::RANGE_SIZE; - let meta_start = usize::from_be_bytes(v[start..mid].try_into().unwrap()); - let meta_end = usize::from_be_bytes(v[mid..end].try_into().unwrap()); + let mut meta_start = [0u8; 4]; + meta_start.copy_from_slice(&v[start..mid]); + let mut meta_end = [0u8; 4]; + meta_end.copy_from_slice(&v[mid..end]); + + let meta_start = u32::from_be_bytes(meta_start) as usize; + let meta_end = u32::from_be_bytes(meta_end) as usize; (ism, v[meta_start..meta_end].to_vec().into()) }) @@ -140,13 +178,44 @@ impl AggregateIsmMetadata { } } -// impl From for HexBinary { -// fn from(v: AggregateIsmMetadata) -> Self { -// v.0.into_iter().fold(vec![], |acc, (ism, metaedata)| { +impl From for HexBinary { + fn from(v: AggregateMetadata) -> Self { + let pos_start = v.0.len() * AggregateMetadata::RANGE_SIZE * 2; + + let ls: Vec<( + [u8; AggregateMetadata::RANGE_SIZE], + [u8; AggregateMetadata::RANGE_SIZE], + HexBinary, + )> = + v.0.values() + .fold(vec![] as Vec<(usize, usize, HexBinary)>, |mut acc, m| { + let l = acc.last().map(|v| v.1).unwrap_or(pos_start); + + acc.push((l, l + m.len(), m.clone())); + acc + }) + .into_iter() + .map(|(start, end, metadata)| { + ( + clone_into_array(&start.to_be_bytes()[AggregateMetadata::RANGE_SIZE..]), + clone_into_array(&end.to_be_bytes()[AggregateMetadata::RANGE_SIZE..]), + metadata, + ) + }) + .collect(); -// }) -// } -// } + let mut pos = vec![]; + let mut metadata = vec![]; + + for (start, end, meta) in ls { + pos.extend_from_slice(&start); + pos.extend_from_slice(&end); + metadata.extend_from_slice(meta.as_slice()); + } + + [pos, metadata].concat().into() + } +} #[cw_serde] pub struct IGPMetadata { @@ -198,10 +267,27 @@ impl IGPMetadata { #[cfg(test)] mod test { - use ibcx_test_utils::hex; + use ibcx_test_utils::{addr, gen_bz, hex}; use super::*; + #[test] + fn test_aggregate() { + let set = vec![ + (addr("test1"), gen_bz(12)), + (addr("test2"), gen_bz(12)), + (addr("test3"), gen_bz(12)), + ]; + + let metadata = AggregateMetadata::new(set); + let isms = metadata.0.clone().into_keys().collect(); + + let metadata_bz: HexBinary = metadata.clone().into(); + + let new_metadata = AggregateMetadata::from_hex(metadata_bz, isms); + assert_eq!(metadata, new_metadata); + } + #[test] fn test_message_id_multisig_metadata() { let testdata = hex("fadafdf4db5e6264d450bafa5951b2180b8fe8aac2e012f280784ae841e9a7f732a2601709a27a5e370a59f98a67b5da6baa522b6421edf2ea240d94d84511a800000000df4eaf1947af0858139b90054561d5ab2a423b4ad8d75a5ec7f9e860fd3de1bb3924e2593e29b595aae2717538c0af6d6ae9fc20477da49d223a0d928a1efb311bdf4eaf1947af0858139b90054561d5ab2a423b4ad8d75a5ec7f9e860fd3de1bb3924e2593e29b595aae2717538c0af6d6ae9fc20477da49d223a0d928a1efb311b"); diff --git a/packages/interface/src/warp/cw20.rs b/packages/interface/src/warp/cw20.rs index 6b20cf87..914cc1fc 100644 --- a/packages/interface/src/warp/cw20.rs +++ b/packages/interface/src/warp/cw20.rs @@ -1,9 +1,11 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::HexBinary; +use cosmwasm_std::{HexBinary, Uint128}; use crate::{ + connection::{ConnectionMsg, ConnectionQueryMsg}, core, - ownable::OwnableQueryMsg, + ism::IsmSpecifierQueryMsg, + ownable::{OwnableMsg, OwnableQueryMsg}, router::{self, RouterQuery}, }; @@ -42,24 +44,21 @@ pub struct InstantiateMsg { pub mailbox: String, } -#[cw_serde] -pub enum ReceiveMsg { - // transfer to remote - TransferRemote { - dest_domain: u32, - recipient: HexBinary, - }, -} - #[cw_serde] pub enum ExecuteMsg { + Ownable(OwnableMsg), Router(router::RouterMsg), + Connection(ConnectionMsg), - /// handle transfer remote + // handle transfer remote Handle(core::HandleMsg), - // cw20 receiver - Receive(cw20::Cw20ReceiveMsg), + // transfer to remote + TransferRemote { + dest_domain: u32, + recipient: HexBinary, + amount: Uint128, + }, } #[cw_serde] @@ -67,6 +66,12 @@ pub enum ExecuteMsg { #[query_responses(nested)] pub enum QueryMsg { Ownable(OwnableQueryMsg), + Router(RouterQuery), + + Connection(ConnectionQueryMsg), + TokenDefault(TokenWarpDefaultQueryMsg), + + IsmSpecifier(IsmSpecifierQueryMsg), } diff --git a/packages/interface/src/warp/native.rs b/packages/interface/src/warp/native.rs index a880be38..254516e3 100644 --- a/packages/interface/src/warp/native.rs +++ b/packages/interface/src/warp/native.rs @@ -1,9 +1,11 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::HexBinary; +use cosmwasm_std::{HexBinary, Uint128}; use crate::{ + connection::{ConnectionMsg, ConnectionQueryMsg}, core, - ownable::OwnableQueryMsg, + ism::IsmSpecifierQueryMsg, + ownable::{OwnableMsg, OwnableQueryMsg}, router::{RouterMsg, RouterQuery}, }; @@ -52,7 +54,9 @@ pub struct InstantiateMsg { #[cw_serde] pub enum ExecuteMsg { + Ownable(OwnableMsg), Router(RouterMsg), + Connection(ConnectionMsg), // handle transfer remote Handle(core::HandleMsg), @@ -61,6 +65,7 @@ pub enum ExecuteMsg { TransferRemote { dest_domain: u32, recipient: HexBinary, + amount: Uint128, }, } @@ -72,7 +77,11 @@ pub enum QueryMsg { Router(RouterQuery), + Connection(ConnectionQueryMsg), + TokenDefault(TokenWarpDefaultQueryMsg), + + IsmSpecifier(IsmSpecifierQueryMsg), } mod as_str { diff --git a/scripts/.env.example b/scripts/.env.example deleted file mode 100644 index c2c3b2df..00000000 --- a/scripts/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -SIGNING_ADDRESS="" -SIGNING_MNEMONIC="" diff --git a/scripts/.gitignore b/scripts/.gitignore index c9358322..ffbc4e4d 100644 --- a/scripts/.gitignore +++ b/scripts/.gitignore @@ -1,4 +1,6 @@ /dist -/context .env +config.yaml +config.*.yaml +!config.example.yaml /node_modules diff --git a/scripts/action/deploy.ts b/scripts/action/deploy.ts new file mode 100644 index 00000000..3df56508 --- /dev/null +++ b/scripts/action/deploy.ts @@ -0,0 +1,276 @@ +import { writeFileSync } from "fs"; + +import { loadContext } from "../src/load_context"; +import { Client, HookType, config, getSigningClient } from "../src/config"; + +import { ContractFetcher } from "./fetch"; +import { Context } from "../src/types"; +import { Contracts, deploy_ism } from "../src/deploy"; + +const name = (c: any) => c.contractName; +const addr = (ctx: Context, c: any) => ctx.contracts[name(c)].address!; + +async function main() { + const client = await getSigningClient(config); + + let ctx = loadContext(config.network.id); + + const contracts = new ContractFetcher(ctx, client).getContracts(); + const { + core: { mailbox }, + mocks, + } = contracts; + + ctx = await deploy_core(ctx, client, contracts); + ctx = await deploy_igp(ctx, client, contracts); + ctx = await deploy_ism_hook(ctx, client, contracts); + + // init test mock msg receiver + ctx.contracts[name(mocks.receiver)] = await mocks.receiver.instantiate({ + hrp: config.network.hrp, + }); + + // pre-setup + await client.wasm.executeMultiple( + client.signer, + [ + { + contractAddress: addr(ctx, mailbox), + msg: { + set_default_ism: { + ism: ctx.contracts["hpl_default_ism"].address!, + }, + }, + }, + { + contractAddress: addr(ctx, mailbox), + msg: { + set_default_hook: { + hook: ctx.contracts["hpl_default_hook"].address!, + }, + }, + }, + { + contractAddress: addr(ctx, mailbox), + msg: { + set_required_hook: { + hook: ctx.contracts["hpl_required_hook"].address!, + }, + }, + }, + ], + "auto" + ); + + writeFileSync("./save.json", JSON.stringify(ctx, null, 2)); +} + +const deploy_core = async ( + ctx: Context, + client: Client, + { core: { mailbox, va } }: Contracts +): Promise => { + // init mailbox + ctx.contracts[name(mailbox)] = await mailbox.instantiate({ + hrp: config.network.hrp, + owner: client.signer, + domain: config.network.domain, + }); + + // init validator announce + ctx.contracts[name(va)] = await va.instantiate({ + hrp: config.network.hrp, + mailbox: addr(ctx, mailbox), + }); + + return ctx; +}; + +const deploy_igp = async ( + ctx: Context, + client: Client, + { igp }: Contracts +): Promise => { + // init igp + ctx.contracts[name(igp.core)] = await igp.core.instantiate({ + hrp: config.network.hrp, + owner: client.signer, + gas_token: config.deploy.igp.token || config.network.gas.denom, + beneficiary: client.signer, + }); + + // init igp oracle + ctx.contracts[name(igp.oracle)] = await igp.oracle.instantiate({ + owner: client.signer, + }); + + await client.wasm.execute( + client.signer, + addr(ctx, igp.oracle), + { + set_remote_gas_data_configs: { + configs: Object.entries(config.deploy.igp.configs).map( + ([domain, v]) => ({ + remote_domain: Number(domain), + token_exchange_rate: v.exchange_rate.toString(), + gas_price: v.gas_price.toString(), + }) + ), + }, + }, + "auto" + ); + + await client.wasm.execute( + client.signer, + addr(ctx, igp.core), + { + router: { + set_routes: { + set: Object.keys(config.deploy.igp.configs).map((domain) => ({ + domain: Number(domain), + route: addr(ctx, igp.oracle), + })), + }, + }, + }, + "auto" + ); + + return ctx; +}; + +const deploy_ism_hook = async ( + ctx: Context, + client: Client, + contracts: Contracts +) => { + ctx.contracts["hpl_default_ism"] = { + ...ctx.contracts[`hpl_ism_${config.deploy.ism?.type || "multisig"}`], + + address: await deploy_ism( + client, + config.deploy.ism || { + type: "multisig", + owner: "", + validators: { + 5: { + addrs: [client.signer_addr], + threshold: 1, + }, + }, + }, + contracts + ), + }; + + ctx.contracts["hpl_default_hook"] = { + ...ctx.contracts[ + config.deploy.hooks?.default?.type && + config.deploy.hooks?.default?.type !== "mock" + ? `hpl_hook_${config.deploy.hooks.default.type}` + : "hpl_test_mock_hook" + ], + + address: await deploy_hook( + ctx, + client, + config.deploy.hooks?.default || { type: "mock" }, + contracts + ), + }; + + ctx.contracts["hpl_required_hook"] = { + ...ctx.contracts[ + config.deploy.hooks?.required?.type && + config.deploy.hooks?.required?.type !== "mock" + ? `hpl_hook_${config.deploy.hooks.required.type}` + : "hpl_test_mock_hook" + ], + + address: await deploy_hook( + ctx, + client, + config.deploy.hooks?.required || { type: "mock" }, + contracts + ), + }; + + return ctx; +}; + +const deploy_hook = async ( + ctx: Context, + client: Client, + hook: HookType, + contracts: Contracts +): Promise => { + const { + core: { mailbox }, + hooks, + igp, + mocks, + } = contracts; + + switch (hook.type) { + case "aggregate": + const aggregate_hook_res = await hooks.aggregate.instantiate({ + owner: hook.owner === "" ? client.signer : hook.owner, + hooks: await Promise.all( + hook.hooks.map((v) => deploy_hook(ctx, client, v, contracts)) + ), + }); + + return aggregate_hook_res.address!; + + case "merkle": + const merkle_hook_res = await hooks.merkle.instantiate({ + owner: hook.owner === "" ? client.signer : hook.owner, + mailbox: addr(ctx, mailbox), + }); + + return merkle_hook_res.address!; + + case "mock": + const mock_hook_res = await mocks.hook.instantiate({}); + + return mock_hook_res.address!; + + case "pausable": + const pausable_hook_res = await hooks.pausable.instantiate({ + owner: hook.owner === "" ? client.signer : hook.owner, + }); + + return pausable_hook_res.address!; + + case "igp": + return ctx.contracts[name(igp.core)].address!; + + case "routing": + const routing_hook_res = await hooks.routing.instantiate({ + owner: hook.owner === "" ? client.signer : hook.owner, + }); + + await client.wasm.execute( + client.signer, + routing_hook_res.address!, + { + router: { + set_routes: { + set: await Promise.all( + Object.entries(hook.hooks).map(async ([domain, v]) => { + const route = await deploy_hook(ctx, client, v, contracts); + return { domain, route }; + }) + ), + }, + }, + }, + "auto" + ); + default: + throw new Error("invalid hook type"); + } +}; + +main().catch(console.error); diff --git a/scripts/action/fetch.ts b/scripts/action/fetch.ts new file mode 100644 index 00000000..270968d4 --- /dev/null +++ b/scripts/action/fetch.ts @@ -0,0 +1,87 @@ +import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate"; +import { Context } from "../src/types"; +import { Client } from "../src/config"; +import { Contracts } from "../src/deploy"; +import { + HplMailbox, + HplValidatorAnnounce, + HplHookAggregate, + HplHookMerkle, + HplHookPausable, + HplHookRouting, + HplHookRoutingCustom, + HplIgp, + HplIgpOracle, + HplIsmAggregate, + HplIsmMultisig, + HplIsmRouting, + HplTestMockHook, + HplTestMockMsgReceiver, + HplWarpCw20, + HplWarpNative, +} from "../src/contracts"; + +type Const = new ( + address: string | undefined, + codeId: number | undefined, + digest: string, + signer: string, + client: SigningCosmWasmClient +) => T; + +export class ContractFetcher { + constructor(private ctx: Context, private client: Client) {} + + public get(f: Const, name: string): T { + return new f( + this.ctx.contracts[name].address, + this.ctx.contracts[name].codeId, + this.ctx.contracts[name].digest, + this.client.signer, + this.client.wasm + ); + } + + public getContracts(): Contracts { + return { + core: { + mailbox: this.get(HplMailbox, "hpl_mailbox"), + va: this.get(HplValidatorAnnounce, "hpl_validator_announce"), + }, + hooks: { + aggregate: this.get(HplHookAggregate, "hpl_hook_aggregate"), + merkle: this.get(HplHookMerkle, "hpl_hook_merkle"), + pausable: this.get(HplHookPausable, "hpl_hook_pausable"), + routing: this.get(HplHookRouting, "hpl_hook_routing"), + routing_custom: this.get( + HplHookRoutingCustom, + "hpl_hook_routing_custom" + ), + routing_fallback: this.get( + HplHookRoutingCustom, + "hpl_hook_routing_fallback" + ), + }, + igp: { + core: this.get(HplIgp, "hpl_igp"), + oracle: this.get(HplIgpOracle, "hpl_igp_oracle"), + }, + isms: { + aggregate: this.get(HplIsmAggregate, "hpl_ism_aggregate"), + multisig: this.get(HplIsmMultisig, "hpl_ism_multisig"), + routing: this.get(HplIsmRouting, "hpl_ism_routing"), + }, + mocks: { + hook: this.get(HplTestMockHook, "hpl_test_mock_hook"), + receiver: this.get( + HplTestMockMsgReceiver, + "hpl_test_mock_msg_receiver" + ), + }, + warp: { + cw20: this.get(HplWarpCw20, "hpl_warp_cw20"), + native: this.get(HplWarpNative, "hpl_warp_native"), + }, + }; + } +} diff --git a/scripts/action/ism.ts b/scripts/action/ism.ts new file mode 100644 index 00000000..897a8d3f --- /dev/null +++ b/scripts/action/ism.ts @@ -0,0 +1,109 @@ +import { Command } from "commander"; +import { ExecuteResult } from "@cosmjs/cosmwasm-stargate"; + +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, + HplIsmAggregate, +} from "../src/contracts"; + +const program = new Command(); + +program.name("Mailbox CLI").version(version); + +program + .command("get-ism") + .argument("", "recipient address in bech32") + .action(makeHandler("get-ism")); + +program + .command("show") + .argument("", "ism address in bech32") + .argument("", "origin domain to be used when multisig") + .action(makeHandler("show-ism")); + +program.parseAsync(process.argv).catch(console.error); + +const parseWasmEventLog = (res: ExecuteResult) => { + return ( + res.events + // .filter((v) => v.type.startsWith("wasm")) + .map((v) => ({ + "@type": v.type.slice(5), + ...Object.fromEntries(v.attributes.map((x) => [x.key, x.value])), + })) + ); +}; + +function makeHandler( + action: "get-ism" | "show-ism" +): (...args: any[]) => void | Promise { + const ctx = loadContext(config.network.id); + + const loadDeps = async () => { + const client = await getSigningClient(config); + 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 hook_merkle = fetcher.get(HplHookMerkle, "hpl_hook_merkle"); + const hook_aggregate = fetcher.get(HplIsmAggregate, "hpl_hook_aggregate"); + + return { + client, + mailbox, + igp: { core: igp, oracle: igp_oracle }, + hook: { merkle: hook_merkle, aggregate: hook_aggregate }, + }; + }; + + switch (action) { + case "get-ism": + return async (recipient_addr: string) => { + const { mailbox } = await loadDeps(); + + const ism = await mailbox.query({ mailbox: { default_ism: {} } }); + console.log("Default ISM on mailbox is", ism); + + const recipientIsm = await mailbox.query({ + mailbox: { recipient_ism: { recipient_addr } }, + }); + + console.log("Recipient ISM is ", recipientIsm); + }; + case "show-ism": + return async (ism_addr: string, originDomain?: string) => { + // Generic info + const { client } = await loadDeps(); + const ism = await client.wasm.queryContractSmart(ism_addr, { + ism: { + module_type: {}, + }, + }); + switch (ism.type) { + case "message_id_multisig": + const msig = await client.wasm.queryContractSmart(ism_addr, { + multisig_ism: { + enrolled_validators: { + domain: Number(originDomain), + }, + }, + }); + const owner = await client.wasm.queryContractSmart(ism_addr, { + ownable: { get_owner: {} }, + }); + console.log(msig, owner); + break; + + default: + break; + } + }; + } +} diff --git a/scripts/action/mailbox.ts b/scripts/action/mailbox.ts new file mode 100644 index 00000000..2509708d --- /dev/null +++ b/scripts/action/mailbox.ts @@ -0,0 +1,103 @@ +import { Command } from "commander"; +import { ExecuteResult } from "@cosmjs/cosmwasm-stargate"; + +import { version } from "../package.json"; +import { config, getSigningClient } from "../src/config"; +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(); + +program.name("Mailbox CLI").version(version); + +program + .command("dispatch") + .argument("", 'destination domain, e.g. "5"') + .argument("", "recipient address in hex") + .argument("", "message body in utf-8") + .action(makeHandler("dispatch")); + +program + .command("process") + .argument("", "metadata in hex") + .argument("", "message body in hex") + .action(makeHandler("process")); + +program.parseAsync(process.argv).catch(console.error); + +const parseWasmEventLog = (res: ExecuteResult) => { + return ( + res.events + // .filter((v) => v.type.startsWith("wasm")) + .map((v) => ({ + "@type": v.type.slice(5), + ...Object.fromEntries(v.attributes.map((x) => [x.key, x.value])), + })) + ); +}; + +function makeHandler( + action: "dispatch" | "process" +): (...args: any[]) => void | Promise { + const ctx = loadContext(config.network.id); + + const loadDeps = async () => { + const client = await getSigningClient(config); + 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 hook_merkle = fetcher.get(HplHookMerkle, "hpl_hook_merkle"); + const hook_aggregate = fetcher.get(HplIsmAggregate, "hpl_hook_aggregate"); + + return { + client, + mailbox, + igp: { core: igp, oracle: igp_oracle }, + hook: { merkle: hook_merkle, aggregate: hook_aggregate }, + }; + }; + + switch (action) { + case "dispatch": + return async ( + dest_domain: string, + recipient_addr: string, + msg_body: string + ) => { + const { mailbox } = await loadDeps(); + + const res = await mailbox.execute( + { + dispatch: { + dest_domain: Number(dest_domain), + recipient_addr: addPad(recipient_addr), + msg_body: Buffer.from(msg_body, "utf-8").toString("hex"), + }, + }, + [{ denom: "token", amount: "26000000" }] + ); + console.log(parseWasmEventLog(res)); + }; + case "process": + return async (metadata: string, msg_body: string) => { + const { mailbox } = await loadDeps(); + + const res = await mailbox.execute({ + process: { + metadata, + msg_body, + }, + }); + console.log(parseWasmEventLog(res)); + }; + } +} diff --git a/scripts/action/migrate.ts b/scripts/action/migrate.ts new file mode 100644 index 00000000..05d8a16a --- /dev/null +++ b/scripts/action/migrate.ts @@ -0,0 +1,75 @@ +import "reflect-metadata"; + +import { Event } from "@cosmjs/cosmwasm-stargate"; +import { config, getSigningClient } from "../src/config"; +import { loadContext } from "../src/load_context"; +import { ContractFetcher } from "./fetch"; + +const parseEventLog = (events: readonly Event[]) => { + return events.map((v) => ({ + "@type": v.type.slice(5), + ...Object.fromEntries(v.attributes.map((x) => [x.key, x.value])), + })); +}; + +async function main() { + const client = await getSigningClient(config); + + const ctx = loadContext(config.network.id); + + const contracts = new ContractFetcher(ctx, client).getContracts(); + + const migrations: [string, number][] = [ + [ + "neutron1q75ky8reksqzh0lkhk9k3csvjwv74jjquahrj233xc7dvzz5fv4qtvw0qg", + contracts.isms.multisig.codeId!, + ], + [ + "neutron12p8wntzra3vpfcqv05scdx5sa3ftaj6gjcmtm7ynkl0e6crtt4ns8cnrmx", + contracts.igp.core.codeId!, + ], + [ + "neutron17w4q6efzym3p4c6umyp4cjf2ustjtmwfqdhd7rt2fpcpk9fmjzsq0kj0f8", + contracts.core.va.codeId!, + ], + ]; + + for (const [addr, code_id] of migrations) { + const contract_info = await client.wasm.getContract(addr); + + if (!contract_info.admin) { + console.log(`skipping ${addr} as it has no admin`); + continue; + } + + if (contract_info.admin !== client.signer) { + console.log( + `skipping ${addr} as it is not admin. actual: ${contract_info.admin}` + ); + continue; + } + + const migrate_resp = await client.wasm.migrate( + client.signer, + addr, + code_id, + {}, + "auto" + ); + console.log(parseEventLog(migrate_resp.events)); + } + + const set_gas_resp = await client.wasm.execute( + client.signer, + "neutron12p8wntzra3vpfcqv05scdx5sa3ftaj6gjcmtm7ynkl0e6crtt4ns8cnrmx", + { + set_default_gas: { + gas: "200000", + }, + }, + "auto" + ); + console.log(parseEventLog(set_gas_resp.events)); +} + +main().catch(console.error); diff --git a/scripts/action/multisig.ts b/scripts/action/multisig.ts new file mode 100644 index 00000000..f53ae1b7 --- /dev/null +++ b/scripts/action/multisig.ts @@ -0,0 +1,175 @@ +import { Command } from "commander"; + +import { version } from "../package.json"; +import { loadContext } from "../src/load_context"; +import { config } from "../src/config"; +import { fromBech32 } from "@cosmjs/encoding"; +import { Secp256k1, keccak256 } from "@cosmjs/crypto"; +import { readFileSync, writeFileSync } from "fs"; +import { DirectSecp256k1Wallet } from "@cosmjs/proto-signing"; + +type CheckpointInfo = { + origin_domain: number; + origin_merkle_tree: string; + merkle_root: string; + merkle_index: number; +}; + +const toHex = (v: Uint8Array): string => { + return Buffer.from(v).toString("hex"); +}; + +const fromHex = (v: string): Uint8Array => { + return Buffer.from(v, "hex"); +}; + +const u8 = (v: string): Uint8Array => { + return fromHex(Number(v).toString(16).padStart(8, "0")); +}; + +const program = new Command(); +const common = { output: `${process.cwd()}/signature.json` }; + +program.name("Multisig CLI").version(version); + +program + .command("sign") + .argument("", 'origin domain, e.g. "5"') + .argument("", "merkle root in hex") + .argument("", "merkle index") + .argument("", "message id in hex") + .option("-o, --output ", "output file", common.output) + .option("-p --prefix ", "bech32 prefix", "dual") + .requiredOption("-k --key ", "private key") + .action(sign); + +program + .command("join") + .argument("", "signature files (comma separated)") + .option("-o, --output ", "output file", common.output) + .option("-p --prefix ", "bech32 prefix", "dual") + .action(join); + +program.parseAsync(process.argv).catch(console.error); + +async function sign( + origin_domain_str: string, + merkle_root: string, + merkle_index_str: string, + message_id: string, + options: { output: string; prefix: string; key: string } +) { + const ctx = loadContext(config.network.id); + const origin_domain = u8(origin_domain_str); + const merkle_index = u8(merkle_index_str); + + const origin_merkle_str = ctx.contracts.hpl_hook_merkle.address!; + const origin_merkle = Buffer.from(fromBech32(origin_merkle_str).data); + + const domain_hash = keccak256( + Buffer.concat([ + origin_domain, + origin_merkle, + Buffer.from("HYPERLANE", "utf-8"), + ]) + ); + + const multisig_hash = keccak256( + Buffer.concat([ + domain_hash, + fromHex(merkle_root), + merkle_index, + fromHex(message_id), + ]) + ); + + const verify_digest = keccak256( + Buffer.concat([ + Buffer.from(`\x19Ethereum Signed Message:\n${multisig_hash.length}`), + multisig_hash, + ]) + ); + + const keypair = await Secp256k1.makeKeypair(fromHex(options.key)); + const key = await DirectSecp256k1Wallet.fromKey( + keypair.privkey, + options.prefix + ); + + const [{ address }] = await key.getAccounts(); + + const signature = await Secp256k1.createSignature( + verify_digest, + keypair.privkey + ); + + type Output = { + address: string; + signature: string; + } & CheckpointInfo; + + const output: Output = { + origin_domain: Number(origin_domain_str), + origin_merkle_tree: origin_merkle.toString("hex"), + merkle_root, + merkle_index: Number(merkle_index_str), + address: address, + signature: toHex(signature.toFixedLength()), + }; + + writeFileSync(options.output, JSON.stringify(output, null, 2)); +} + +async function join( + signature_paths: string[], + options: { output: string; prefix: string } +) { + const ctx = loadContext(config.network.id); + + const origin_merkle_str = ctx.contracts.hpl_hook_merkle.address!; + const origin_merkle = Buffer.from(fromBech32(origin_merkle_str).data); + + type Output = { + address: string; + signature: string; + } & CheckpointInfo; + + type Joined = { + signatures: Record; + } & CheckpointInfo; + + let joined: Joined | null = null; + + for (const path of signature_paths) { + const output: Output = JSON.parse(readFileSync(path, "utf-8")); + + if (joined) { + joined.signatures[output.address!] = output.signature!; + continue; + } + + joined = { + origin_domain: output.origin_domain, + origin_merkle_tree: output.origin_merkle_tree, + merkle_root: output.merkle_root, + merkle_index: output.merkle_index, + signatures: { + [output.address]: output.signature, + }, + }; + } + + if (!joined) { + console.error("no signature given"); + return; + } + + const metadata = Buffer.concat([ + fromHex(joined.origin_merkle_tree), + fromHex(joined.merkle_root), + u8(joined.merkle_index.toString()), + Buffer.concat(Object.values(joined.signatures).map((v) => fromHex(v))), + ]); + + console.log(metadata.toString("hex")); +} diff --git a/scripts/action/warp.ts b/scripts/action/warp.ts new file mode 100644 index 00000000..0e1587fe --- /dev/null +++ b/scripts/action/warp.ts @@ -0,0 +1,175 @@ +import { version } from "../package.json"; +import { loadContext } from "../src/load_context"; +import { config, getSigningClient } from "../src/config"; +import { ContractFetcher } from "./fetch"; +import { addPad } from "../src/conv"; +import { ExecuteResult } from "@cosmjs/cosmwasm-stargate"; +import { Command } from "commander"; + +const program = new Command(); + +program.name("Warp CLI").version(version); + +program + .command("new") + .argument("", 'token denom, e.g. "untrn"') + .option( + "--token-mode ", + 'token mode, e.g. "collateral" or "bridged"', + "collateral" + ) + .action(create); + +program + .command("set-ism") + .argument("
", "address of internal warp route") + .argument("", "address of ISM") + .action(setIsm); + +program + .command("link") + .argument("
", "address of internal warp route") + .argument("", "domain of external chain, e.g. 5 (goerli)") + .argument("", "address of external route") + .action(link); + +program + .command("transfer") + .argument("
", "address of internal warp route") + .argument("", "domain of external chain, e.g. 5 (goerli)") + .argument("", "recipient address") + .argument("") + .action(transfer); + +program.parseAsync(process.argv).catch(console.error); + +const parseWasmEventLog = (res: ExecuteResult) => { + return ( + res.events + // .filter((v) => v.type.startsWith("wasm")) + .map((v) => ({ + "@type": v.type.slice(5), + ...Object.fromEntries(v.attributes.map((x) => [x.key, x.value])), + })) + ); +}; + +async function create( + denom: string, + { tokenMode }: { tokenMode: "collateral" | "bridged" } +) { + const client = await getSigningClient(config); + const ctx = loadContext(config.network.id); + + const fetcher = new ContractFetcher(ctx, client); + const { + core: { mailbox }, + warp, + } = fetcher.getContracts(); + + switch (tokenMode) { + case "collateral": + const ibc_route = await warp.native.instantiate({ + token: { + collateral: { + denom, + }, + }, + hrp: config.network.hrp, + owner: client.signer, + mailbox: mailbox.address!, + }); + + console.log("ibc_route", ibc_route); + return; + case "bridged": + throw Error("not implemented"); + } +} + +async function setIsm(address: string, ism: string) { + const client = await getSigningClient(config); + const resp = await client.wasm.execute( + client.signer, + address, + { + connection: { + set_ism: { + ism, + }, + }, + }, + "auto" + ); + console.log(parseWasmEventLog(resp)); + console.log(resp.transactionHash); +} +async function link(address: string, domain: string, external_route: string) { + const client = await getSigningClient(config); + const resp = await client.wasm.execute( + client.signer, + address, + { + router: { + set_route: { + set: { + domain: Number(domain), + route: addPad(external_route), + }, + }, + }, + }, + "auto" + ); + console.log(parseWasmEventLog(resp)); + console.log(resp.transactionHash); +} + +async function transfer( + address: string, + domain: string, + recipient: string, + amount: string +) { + const client = await getSigningClient(config); + + const { + type: { + native: { + fungible: { denom }, + }, + }, + }: { + type: { + native: { + fungible: { + denom: string; + }; + }; + }; + } = await client.wasm.queryContractSmart(address, { + token_default: { + token_type: {}, + }, + }); + + const resp = await client.wasm.execute( + client.signer, + address, + { + transfer_remote: { + dest_domain: Number(domain), + recipient: addPad(recipient), + amount, + }, + }, + "auto", + undefined, + [ + { amount, denom }, + { amount: "100", denom: "untrn" }, + ] + ); + console.log(parseWasmEventLog(resp)); + console.log(resp.transactionHash); +} diff --git a/scripts/config.example.yaml b/scripts/config.example.yaml new file mode 100644 index 00000000..b4a213c6 --- /dev/null +++ b/scripts/config.example.yaml @@ -0,0 +1,8 @@ +network: + id: "localosmosis" + hrp: "osmo" + url: "http://localhost:26657" + gas: "0.025uosmo" + domain: "2303" + +signer: deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef diff --git a/scripts/context/duality-devnet.json b/scripts/context/duality-devnet.json new file mode 100644 index 00000000..e0614116 --- /dev/null +++ b/scripts/context/duality-devnet.json @@ -0,0 +1,97 @@ +{ + "contracts": { + "hpl_hook_merkle": { + "codeId": 131, + "digest": "0fa54766b2e8cb3e87ef076fbe0d50e09a21c23b0ab5671b1d2c548571bd2728" + }, + "hpl_igp": { + "address": "dual14cu2z6xw62cumt7hmfw7977jyaymr26jazxdpvfp7ag2dss29q2q700yum", + "codeId": 147, + "digest": "ed02f45ba7566523428c483a99840ec803b4a8c4a61504c85c70713adfe4d912" + }, + "hpl_igp_oracle": { + "address": "dual1alp24grk7e5f3msv0apk68hehspglpr7y7upfrvkpdfu7c84sqtq8ze5p2", + "codeId": 137, + "digest": "be0546baf9b1c2eaf28a8111f784521ee011f69a474098162bbda73a2db68e63" + }, + "hpl_ism_multisig": { + "codeId": 139, + "digest": "168a875843bd6b13420e289e6a089e1c566d62f95e9b9fb4bcec0c61966940f4" + }, + "hpl_mailbox": { + "address": "dual1mveu0r9rj4qa6aqxt8almpha6cqluu397y8jd6r4jhzm3hmtmndq8lvk47", + "codeId": 141, + "digest": "66dec947cc99c61c9edf56576e0b5c0e675b0d86e39495acf6f05db1657fabd1" + }, + "hpl_test_mock_hook": { + "codeId": 142, + "digest": "df1490919845c9a12b0e63488cbf83b242117493ad4b2b0d2473f6460bb3c074" + }, + "hpl_test_mock_msg_receiver": { + "address": "dual1rvtgvc38sfd9zehtgsp3eh8k269naq949u5qdcqm3x35mjg2uctqfdn3yq", + "codeId": 143, + "digest": "d0f8bc913e07df303911222970e09dd3abeef97399cb1c554d76cf98caa7f37a" + }, + "hpl_validator_announce": { + "address": "dual1982lwq4rt4qntkv2hafvvtwupn7hgqqkv0kpf55yahlh6pqeldvqd24lld", + "codeId": 144, + "digest": "db7afc67aa15c4f287af98c7836b3acf2016b96c6aa1eee01458bc11e6732178" + }, + "hpl_warp_cw20": { + "codeId": 145, + "digest": "5fdafe6f066e2f646b88a6ec29ae401012672fb8f674a18808771b5051beb7fc" + }, + "hpl_warp_native": { + "codeId": 146, + "digest": "983943571c74276e36a36f62ef9c6c54dc6de2c57fb76af4a0de37a0b5fef3ec" + }, + "hpl_warp_native_ibc": { + "codeId": 32, + "digest": "eed9d7dfa18163de64893cfe88e038cfc82e487561c94b142978efdacc1f9dff" + }, + "hpl_hook_aggregate": { + "codeId": 130, + "digest": "910a823bb524dbbaed9952f484cb242ea837e50b5f7c458c1bf52d16c7de21fb" + }, + "hpl_hook_pausable": { + "codeId": 132, + "digest": "226c4125998bf88666fb5ef7caeb86de28cde42ff679ec4029dedaf02c3a83c7" + }, + "hpl_hook_routing": { + "codeId": 133, + "digest": "55482f9f69d4d6d4784d1a378321ebf72936f0f13c5539ce1598bc9e8ed087f3" + }, + "hpl_hook_routing_custom": { + "codeId": 134, + "digest": "48b7dc080418493f4f6f2737340ffa34bdcc6fd3197a2585bc4eea8eb72ee829" + }, + "hpl_hook_routing_fallback": { + "codeId": 135, + "digest": "6fcf09c7d50fd5bfddbdeb0020625bdab2094237dca4f29e93ae12371a550f6f" + }, + "hpl_ism_aggregate": { + "codeId": 138, + "digest": "3a4adfaed525c40845768e05da436f1ea39f6c9d7e9d47ec2de8457034a229c1" + }, + "hpl_ism_routing": { + "codeId": 140, + "digest": "80bfb6964f9a1e4bd64aa69421b7b27327b00ab503931f0a22e7169d774eaf69" + }, + "hpl_default_ism": { + "codeId": 77, + "digest": "435f13deae9c7b7cb0d244bdbe9ae095c27c69bf81f4b9857be68e8b9174f4de", + "address": "dual192t0g0gu7xg4czamvs2qwr0w3arpfcsa6nsj9tp0g0fjqe6x5wrs35rzmq" + }, + "hpl_default_hook": { + "codeId": 80, + "digest": "7a4413b88f334a0ac0009a68887cee1cd160fbb83e3b8dd87bd4489b44d28bdb", + "address": "dual1uu64dmvdt6s2a0w9xa066cr42q35um8arsfh4qw8wspcfsmy3jasnjjzkt" + }, + "hpl_required_hook": { + "codeId": 68, + "digest": "1d0d021c3200bdae970d4a17b28aa3d35136953e3a931c77acb422c466d3a2c1", + "address": "dual1nz2n6vfk5fjlex2qquupyaqhaucr4xn30lm0teqwh0r0zv0v9xhsky7h3a" + } + }, + "address": "dual1dwnrgwsf5c9vqjxsax04pdm0mx007yrraj2dgn" +} \ No newline at end of file diff --git a/scripts/context/neutron-1.json b/scripts/context/neutron-1.json new file mode 100644 index 00000000..6659dd82 --- /dev/null +++ b/scripts/context/neutron-1.json @@ -0,0 +1,73 @@ +{ + "contracts": { + "hpl_hook_aggregate": { + "codeId": 405, + "digest": "910a823bb524dbbaed9952f484cb242ea837e50b5f7c458c1bf52d16c7de21fb" + }, + "hpl_hook_merkle": { + "codeId": 406, + "digest": "0fa54766b2e8cb3e87ef076fbe0d50e09a21c23b0ab5671b1d2c548571bd2728" + }, + "hpl_hook_pausable": { + "codeId": 407, + "digest": "226c4125998bf88666fb5ef7caeb86de28cde42ff679ec4029dedaf02c3a83c7" + }, + "hpl_hook_routing": { + "codeId": 408, + "digest": "55482f9f69d4d6d4784d1a378321ebf72936f0f13c5539ce1598bc9e8ed087f3" + }, + "hpl_hook_routing_custom": { + "codeId": 409, + "digest": "48b7dc080418493f4f6f2737340ffa34bdcc6fd3197a2585bc4eea8eb72ee829" + }, + "hpl_hook_routing_fallback": { + "codeId": 410, + "digest": "6fcf09c7d50fd5bfddbdeb0020625bdab2094237dca4f29e93ae12371a550f6f" + }, + "hpl_igp": { + "codeId": 433, + "digest": "016156ac0887daca0545eddd7d2adad91ee7049485abe567efbbf8476282a2ef" + }, + "hpl_igp_oracle": { + "codeId": 412, + "digest": "be0546baf9b1c2eaf28a8111f784521ee011f69a474098162bbda73a2db68e63" + }, + "hpl_ism_aggregate": { + "codeId": 413, + "digest": "3a4adfaed525c40845768e05da436f1ea39f6c9d7e9d47ec2de8457034a229c1" + }, + "hpl_ism_multisig": { + "codeId": 431, + "digest": "e233448e7b3fa54b528740ec882a93a59300fda7c828e829ad04c44f5344e259" + }, + "hpl_ism_routing": { + "codeId": 415, + "digest": "80bfb6964f9a1e4bd64aa69421b7b27327b00ab503931f0a22e7169d774eaf69" + }, + "hpl_mailbox": { + "codeId": 416, + "digest": "66dec947cc99c61c9edf56576e0b5c0e675b0d86e39495acf6f05db1657fabd1" + }, + "hpl_test_mock_hook": { + "codeId": 417, + "digest": "df1490919845c9a12b0e63488cbf83b242117493ad4b2b0d2473f6460bb3c074" + }, + "hpl_test_mock_msg_receiver": { + "codeId": 418, + "digest": "d0f8bc913e07df303911222970e09dd3abeef97399cb1c554d76cf98caa7f37a" + }, + "hpl_validator_announce": { + "codeId": 432, + "digest": "5985741956f51e1353095939c413b5e03d03670f97482783cfd629a72c1b240d" + }, + "hpl_warp_cw20": { + "codeId": 420, + "digest": "5fdafe6f066e2f646b88a6ec29ae401012672fb8f674a18808771b5051beb7fc" + }, + "hpl_warp_native": { + "codeId": 421, + "digest": "983943571c74276e36a36f62ef9c6c54dc6de2c57fb76af4a0de37a0b5fef3ec" + } + }, + "address": "neutron1dwnrgwsf5c9vqjxsax04pdm0mx007yrre4yyvm" +} \ No newline at end of file diff --git a/scripts/deploy.ts b/scripts/deploy.ts deleted file mode 100644 index 584dc5ee..00000000 --- a/scripts/deploy.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { - ExecuteResult, - SigningCosmWasmClient, -} from "@cosmjs/cosmwasm-stargate"; -import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; -import { GasPrice } from "@cosmjs/stargate"; - -import { loadContext } from "./src/load_context"; -import HplMailbox from "./src/contracts/hpl_mailbox"; -import { BaseContract, Context } from "./src/types"; -import HplHookMerkle from "./src/contracts/hpl_hook_merkle"; -import HplTestMockHook from "./src/contracts/hpl_test_mock_hook"; -import HplIgpGasOracle from "./src/contracts/hpl_igp_oracle"; -import HplIgp from "./src/contracts/hpl_igp"; -import HplIsmMultisig from "./src/contracts/hpl_ism_multisig"; -import { writeFileSync } from "fs"; -import HplValidatorAnnounce from "./src/contracts/hpl_validator_announce"; -import HplTestMockMsgReceiver from "./src/contracts/hpl_test_mock_msg_receiver"; - -const NETWORK_ID = process.env.NETWORK_ID || "osmo-test-5"; -const NETWORK_HRP = process.env.NETWORK_HRP || "osmo"; -const NETWORK_URL = - process.env.NETWORK_URL || "https://rpc.osmotest5.osmosis.zone"; -const NETWORK_GAS = process.env.NETWORK_GAS || "0.025uosmo"; - -async function getSigningClient(): Promise<{ - client: SigningCosmWasmClient; - address: string; -}> { - const mnemonic = process.env["SIGNING_MNEMONIC"] as string; - const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { - prefix: NETWORK_HRP, - }); - const [{ address }] = await wallet.getAccounts(); - - const client = await SigningCosmWasmClient.connectWithSigner( - NETWORK_URL, - wallet, - { - gasPrice: GasPrice.fromString(NETWORK_GAS), - } - ); - return { client, address }; -} - -type Const = new ( - address: string | undefined, - codeId: number | undefined, - digest: string, - signer: string, - client: SigningCosmWasmClient -) => T; - -class ContractFetcher { - constructor( - private ctx: Context, - private owner: string, - private client: SigningCosmWasmClient - ) {} - - public get(f: Const, name: string): T { - return new f( - this.ctx.contracts[name].address, - this.ctx.contracts[name].codeId, - this.ctx.contracts[name].digest, - this.owner, - this.client - ); - } -} - -async function main() { - const { client, address: owner } = await getSigningClient(); - - const ctx = loadContext(NETWORK_ID); - - const fetcher = new ContractFetcher(ctx, owner, client); - - const mailbox = fetcher.get(HplMailbox, "hpl_mailbox"); - const va = fetcher.get(HplValidatorAnnounce, "hpl_validator_announce"); - - const hook_merkle = fetcher.get(HplHookMerkle, "hpl_hook_merkle"); - const igp_oracle = fetcher.get(HplIgpGasOracle, "hpl_igp_oracle"); - const igp = fetcher.get(HplIgp, "hpl_igp"); - const ism_multisig = fetcher.get(HplIsmMultisig, "hpl_ism_multisig"); - - const test_mock_hook = fetcher.get(HplTestMockHook, "hpl_test_mock_hook"); - const test_mock_receiver = fetcher.get( - HplTestMockMsgReceiver, - "hpl_test_mock_msg_receiver" - ); - - // init mailbox - ctx.contracts[mailbox.contractName] = await mailbox.instantiate({ - hrp: "dual", - owner, - domain: 33333, - }); - - // init validator announce - ctx.contracts[va.contractName] = await va.instantiate({ - hrp: "dual", - mailbox: ctx.contracts[mailbox.contractName].address, - }); - - // init merkle hook - (required hook) - ctx.contracts[hook_merkle.contractName] = await hook_merkle.instantiate({ - owner: ctx.address!, - mailbox: ctx.contracts[mailbox.contractName].address, - }); - - // init mock hook - (default hook) - ctx.contracts[test_mock_hook.contractName] = await test_mock_hook.instantiate( - {} - ); - - // init igp oracle - ctx.contracts[igp_oracle.contractName] = await igp_oracle.instantiate({}); - - // init igp - ctx.contracts[igp.contractName] = await igp.instantiate({ - hrp: "dual", - owner: ctx.address!, - mailbox: ctx.contracts[mailbox.contractName].address, - gas_token: "token", - beneficiary: ctx.address!, - }); - - // init ism multisig - ctx.contracts[ism_multisig.contractName] = await ism_multisig.instantiate({ - hrp: "dual", - owner: ctx.address!, - }); - - // init test mock msg receiver - ctx.contracts[test_mock_receiver.contractName] = - await test_mock_receiver.instantiate({ hrp: "dual" }); - - // pre-setup - await client.executeMultiple( - owner, - [ - { - contractAddress: ctx.contracts[mailbox.contractName].address!, - msg: { - set_default_ism: { - ism: ctx.contracts[ism_multisig.contractName].address!, - }, - }, - }, - { - contractAddress: ctx.contracts[mailbox.contractName].address!, - msg: { - set_default_hook: { - hook: ctx.contracts[test_mock_hook.contractName].address!, - }, - }, - }, - { - contractAddress: ctx.contracts[mailbox.contractName].address!, - msg: { - set_required_hook: { - hook: ctx.contracts[hook_merkle.contractName].address!, - }, - }, - }, - ], - "auto" - ); - - writeFileSync("./save.json", JSON.stringify(ctx, null, 2)); -} - -main().catch(console.error); diff --git a/scripts/fill.ts b/scripts/fill.ts index 9f165773..e8232353 100644 --- a/scripts/fill.ts +++ b/scripts/fill.ts @@ -10,6 +10,7 @@ import { ARTIFACTS } from "./artifacts"; async function getSigningClient(): Promise<{ client: SigningCosmWasmClient; address: string; + pubkey: string; }> { const mnemonic = process.env["SIGNING_MNEMONIC"] as string; const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { @@ -24,11 +25,14 @@ async function getSigningClient(): Promise<{ gasPrice: GasPrice.fromString("0.025uosmo"), } ); - return { client, address }; + + const pubkey = process.env["SIGNING_PUBKEY"] as string; + + return { client, address, pubkey }; } async function main() { - const { client, address: owner } = await getSigningClient(); + const { client, address: owner, pubkey } = await getSigningClient(); const { hpl_hub: { address: hpl_hub }, @@ -43,6 +47,8 @@ async function main() { let execRes: ExecuteResult; + // =========================== hpl_routing + // =========================== hpl_hub { const originDomain = await client.queryContractSmart(hpl_hub, { diff --git a/scripts/package.json b/scripts/package.json index aac1f75a..8b6796ec 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -18,15 +18,19 @@ "@cosmjs/encoding": "^0.31.0", "@cosmjs/proto-signing": "^0.31.0", "@cosmjs/stargate": "^0.31.0", + "@cosmjs/tendermint-rpc": "0.31.0", "axios": "^1.4.0", "colors": "^1.4.0", + "commander": "^11.1.0", "inversify": "^6.0.1", "readline": "^1.3.0", "reflect-metadata": "^0.1.13" }, "devDependencies": { + "@types/js-yaml": "^4.0.8", "@types/node": "^20.4.4", "ts-node": "^10.9.1", + "ts-yaml": "^1.0.0", "tsx": "^3.13.0", "typescript": "^5.1.6" } diff --git a/scripts/pnpm-lock.yaml b/scripts/pnpm-lock.yaml index cfbbe4b7..3ef0d902 100644 --- a/scripts/pnpm-lock.yaml +++ b/scripts/pnpm-lock.yaml @@ -26,12 +26,18 @@ dependencies: '@cosmjs/stargate': specifier: ^0.31.0 version: 0.31.0 + '@cosmjs/tendermint-rpc': + specifier: 0.31.0 + version: 0.31.0 axios: specifier: ^1.4.0 version: 1.4.0 colors: specifier: ^1.4.0 version: 1.4.0 + commander: + specifier: ^11.1.0 + version: 11.1.0 inversify: specifier: ^6.0.1 version: 6.0.1 @@ -43,12 +49,18 @@ dependencies: version: 0.1.13 devDependencies: + '@types/js-yaml': + specifier: ^4.0.8 + version: 4.0.8 '@types/node': specifier: ^20.4.4 version: 20.4.4 ts-node: specifier: ^10.9.1 version: 10.9.1(@types/node@20.4.4)(typescript@5.1.6) + ts-yaml: + specifier: ^1.0.0 + version: 1.0.0 tsx: specifier: ^3.13.0 version: 3.13.0 @@ -77,10 +89,10 @@ packages: /@cosmjs/amino@0.31.0: resolution: {integrity: sha512-xJ5CCEK7H79FTpOuEmlpSzVI+ZeYESTVvO3wHDgbnceIyAne3C68SvyaKqLUR4uJB0Z4q4+DZHbqW6itUiv4lA==} dependencies: - '@cosmjs/crypto': 0.31.0 - '@cosmjs/encoding': 0.31.0 - '@cosmjs/math': 0.31.0 - '@cosmjs/utils': 0.31.0 + '@cosmjs/crypto': 0.31.1 + '@cosmjs/encoding': 0.31.1 + '@cosmjs/math': 0.31.1 + '@cosmjs/utils': 0.31.1 dev: false /@cosmjs/cosmwasm-launchpad@0.25.6: @@ -151,6 +163,18 @@ packages: libsodium-wrappers-sumo: 0.7.11 dev: false + /@cosmjs/crypto@0.31.1: + resolution: {integrity: sha512-4R/SqdzdVzd4E5dpyEh1IKm5GbTqwDogutyIyyb1bcOXiX/x3CrvPI9Tb4WSIMDLvlb5TVzu2YnUV51Q1+6mMA==} + dependencies: + '@cosmjs/encoding': 0.31.1 + '@cosmjs/math': 0.31.1 + '@cosmjs/utils': 0.31.1 + '@noble/hashes': 1.3.1 + bn.js: 5.2.1 + elliptic: 6.5.4 + libsodium-wrappers-sumo: 0.7.11 + dev: false + /@cosmjs/encoding@0.25.6: resolution: {integrity: sha512-0imUOB8XkUstI216uznPaX1hqgvLQ2Xso3zJj5IV5oJuNlsfDj9nt/iQxXWbJuettc6gvrFfpf+Vw2vBZSZ75g==} dependencies: @@ -167,10 +191,18 @@ packages: readonly-date: 1.0.0 dev: false - /@cosmjs/json-rpc@0.31.0: - resolution: {integrity: sha512-Ix2Cil2qysiLNrX+E0w3vtwCrqxGVq8jklpLA7B2vtMrw7tru/rS65fdFSy8ep0wUNLL6Ud32VXa5K0YObDOMA==} + /@cosmjs/encoding@0.31.1: + resolution: {integrity: sha512-IuxP6ewwX6vg9sUJ8ocJD92pkerI4lyG8J5ynAM3NaX3q+n+uMoPRSQXNeL9bnlrv01FF1kIm8if/f5F7ZPtkA==} dependencies: - '@cosmjs/stream': 0.31.0 + base64-js: 1.5.1 + bech32: 1.1.4 + readonly-date: 1.0.0 + dev: false + + /@cosmjs/json-rpc@0.31.1: + resolution: {integrity: sha512-gIkCj2mUDHAxvmJnHtybXtMLZDeXrkDZlujjzhvJlWsIuj1kpZbKtYqh+eNlfwhMkMMAlQa/y4422jDmizW+ng==} + dependencies: + '@cosmjs/stream': 0.31.1 xstream: 11.14.0 dev: false @@ -200,6 +232,12 @@ packages: bn.js: 5.2.1 dev: false + /@cosmjs/math@0.31.1: + resolution: {integrity: sha512-kiuHV6m6DSB8/4UV1qpFhlc4ul8SgLXTGRlYkYiIIP4l0YNeJ+OpPYaOlEgx4Unk2mW3/O2FWYj7Jc93+BWXng==} + dependencies: + bn.js: 5.2.1 + dev: false + /@cosmjs/proto-signing@0.31.0: resolution: {integrity: sha512-JNlyOJRkn8EKB9mCthkjr6lVX6eyVQ09PFdmB4/DR874E62dFTvQ+YvyKMAgN7K7Dcjj26dVlAD3f6Xs7YOGDg==} dependencies: @@ -212,10 +250,10 @@ packages: long: 4.0.0 dev: false - /@cosmjs/socket@0.31.0: - resolution: {integrity: sha512-WDh9gTyiP3OCXvSAJJn33+Ef3XqMWag+bpR1TdMBxTmlTxuvU+kPy4cf6P2OF+jkkUBEA5Se2EAju0eFbJMT+w==} + /@cosmjs/socket@0.31.1: + resolution: {integrity: sha512-XTtEr+x3WGbqkzoGX0sCkwVqf5n+bBqDwqNgb+DWaBABQxHVRuuainrTVp0Yc91D3Iy2twLQzeBA9OrRxDSerw==} dependencies: - '@cosmjs/stream': 0.31.0 + '@cosmjs/stream': 0.31.1 isomorphic-ws: 4.0.1(ws@7.5.9) ws: 7.5.9 xstream: 11.14.0 @@ -251,16 +289,22 @@ packages: xstream: 11.14.0 dev: false + /@cosmjs/stream@0.31.1: + resolution: {integrity: sha512-xsIGD9bpBvYYZASajCyOevh1H5pDdbOWmvb4UwGZ78doGVz3IC3Kb9BZKJHIX2fjq9CMdGVJHmlM+Zp5aM8yZA==} + dependencies: + xstream: 11.14.0 + dev: false + /@cosmjs/tendermint-rpc@0.31.0: resolution: {integrity: sha512-yo9xbeuI6UoEKIhFZ9g0dvUKLqnBzwdpEc/uldQygQc51j38gQVwFko+6sjmhieJqRYYvrYumcbJMiV6GFM9aA==} dependencies: - '@cosmjs/crypto': 0.31.0 - '@cosmjs/encoding': 0.31.0 - '@cosmjs/json-rpc': 0.31.0 - '@cosmjs/math': 0.31.0 - '@cosmjs/socket': 0.31.0 - '@cosmjs/stream': 0.31.0 - '@cosmjs/utils': 0.31.0 + '@cosmjs/crypto': 0.31.1 + '@cosmjs/encoding': 0.31.1 + '@cosmjs/json-rpc': 0.31.1 + '@cosmjs/math': 0.31.1 + '@cosmjs/socket': 0.31.1 + '@cosmjs/stream': 0.31.1 + '@cosmjs/utils': 0.31.1 axios: 0.21.4 readonly-date: 1.0.0 xstream: 11.14.0 @@ -278,6 +322,10 @@ packages: resolution: {integrity: sha512-nNcycZWUYLNJlrIXgpcgVRqdl6BXjF4YlXdxobQWpW9Tikk61bEGeAFhDYtC0PwHlokCNw0KxWiHGJL4nL7Q5A==} dev: false + /@cosmjs/utils@0.31.1: + resolution: {integrity: sha512-n4Se1wu4GnKwztQHNFfJvUeWcpvx3o8cWhSbNs9JQShEuB3nv3R5lqFBtDCgHZF/emFQAP+ZjF8bTfCs9UBGhA==} + dev: false + /@cspotcode/source-map-support@0.8.1: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -563,6 +611,10 @@ packages: resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} dev: true + /@types/js-yaml@4.0.8: + resolution: {integrity: sha512-m6jnPk1VhlYRiLFm3f8X9Uep761f+CK8mHyS65LutH2OhmBF0BeMEjHgg05usH8PLZMWWc/BUR9RPmkvpWnyRA==} + dev: true + /@types/long@4.0.2: resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} dev: false @@ -585,6 +637,17 @@ packages: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} dev: true + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + dev: true + + /arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + dev: true + /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: false @@ -649,6 +712,11 @@ packages: delayed-stream: 1.0.0 dev: false + /commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + dev: false + /cosmjs-types@0.8.0: resolution: {integrity: sha512-Q2Mj95Fl0PYMWEhA2LuGEIhipF7mQwd9gTQ85DdP9jjjopeoGaDxvmPa5nakNzsq7FnO1DMTatXTAx6bxMH7Lg==} dependencies: @@ -673,6 +741,11 @@ packages: engines: {node: '>=0.4.0'} dev: false + /diff@3.5.0: + resolution: {integrity: sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==} + engines: {node: '>=0.3.1'} + dev: true + /diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} @@ -720,6 +793,12 @@ packages: '@esbuild/win32-x64': 0.18.20 dev: true + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: true + /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: false @@ -844,6 +923,14 @@ packages: resolution: {integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==} dev: false + /js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + dev: true + /libsodium-sumo@0.7.11: resolution: {integrity: sha512-bY+7ph7xpk51Ez2GbE10lXAQ5sJma6NghcIDaSPbM/G9elfrjLa0COHl/7P6Wb/JizQzl5UQontOOP1z0VwbLA==} dev: false @@ -892,6 +979,17 @@ packages: resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} dev: false + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: true + + /mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: true + /object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} @@ -981,6 +1079,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: true + /string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: @@ -1023,6 +1125,28 @@ packages: yn: 3.1.1 dev: true + /ts-node@6.2.0: + resolution: {integrity: sha512-ZNT+OEGfUNVMGkpIaDJJ44Zq3Yr0bkU/ugN1PHbU+/01Z7UV1fsELRiTx1KuQNvQ1A3pGh3y25iYF6jXgxV21A==} + engines: {node: '>=4.2.0'} + hasBin: true + dependencies: + arrify: 1.0.1 + buffer-from: 1.1.2 + diff: 3.5.0 + make-error: 1.3.6 + minimist: 1.2.8 + mkdirp: 0.5.6 + source-map-support: 0.5.21 + yn: 2.0.0 + dev: true + + /ts-yaml@1.0.0: + resolution: {integrity: sha512-g5D8X+8VhhljTWT5M4A9O8+ONj6WNB34E/WUAMr7DBxyC6S8lFJ2tgv24hfOcwz2x0hUIifBlPSf4nnh2NGY6A==} + dependencies: + js-yaml: 3.14.1 + ts-node: 6.2.0 + dev: true + /tsx@3.13.0: resolution: {integrity: sha512-rjmRpTu3as/5fjNq/kOkOtihgLxuIz6pbKdj9xwP4J5jOLkBxw/rjN5ANw+KyrrOXV5uB7HC8+SrrSJxT65y+A==} hasBin: true @@ -1068,6 +1192,11 @@ packages: symbol-observable: 2.0.3 dev: false + /yn@2.0.0: + resolution: {integrity: sha512-uTv8J/wiWTgUTg+9vLTi//leUl5vDQS6uii/emeTb2ssY7vl6QWf2fFbIIGjnhjvbdKlU0ed7QPgY1htTC86jQ==} + engines: {node: '>=4'} + dev: true + /yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} diff --git a/scripts/src/config.ts b/scripts/src/config.ts new file mode 100644 index 00000000..86f7337c --- /dev/null +++ b/scripts/src/config.ts @@ -0,0 +1,150 @@ +import yaml from "js-yaml"; +import { readFileSync } from "fs"; +import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate"; +import { + Tendermint34Client, + Tendermint37Client, + TendermintClient, +} from "@cosmjs/tendermint-rpc"; +import { DirectSecp256k1Wallet } from "@cosmjs/proto-signing"; +import { GasPrice, SigningStargateClient } from "@cosmjs/stargate"; +import { Secp256k1, keccak256 } from "@cosmjs/crypto"; + +export type IsmType = + | { + type: "multisig"; + owner: string; + validators: { + [domain: number]: { addrs: string[]; threshold: number }; + }; + } + | { + type: "aggregate"; + owner: string; + isms: IsmType[]; + } + | { + type: "routing"; + owner: string; + isms: { [domain: number]: IsmType }; + }; + +export type HookType = + | { + type: "merkle"; + owner: string; + } + | { + type: "mock"; + } + | { + type: "pausable"; + owner: string; + } + | { + type: "igp"; + } + | { type: "aggregate"; owner: string; hooks: HookType[] } + | { + type: "routing"; + owner: string; + hooks: { [domain: number]: HookType }; + custom_hooks?: { + [domain: number]: { recipient: string; hook: string }; + }; + fallback_hook?: string; + }; + +export type Config = { + network: { + id: string; + hrp: string; + url: string; + gas: { + price: string; + denom: string; + }; + domain: number; + tm_version?: "34" | "37"; + }; + + signer: string; + + deploy: { + igp: { + token?: string; + configs: { + [domain: number]: { + exchange_rate: number; + gas_price: number; + }; + }; + }; + ism?: IsmType; + hooks?: { + default?: HookType; + required?: HookType; + }; + }; +}; + +export type Client = { + wasm: SigningCosmWasmClient; + stargate: SigningStargateClient; + signer: string; + signer_addr: string; + signer_pubkey: string; +}; + +const path = process.env.CONFIG || `${process.cwd()}/config.yaml`; + +export const config = yaml.load(readFileSync(path, "utf-8")) as Config; + +export async function getSigningClient({ + network, + signer, +}: Config): Promise { + const wallet = await DirectSecp256k1Wallet.fromKey( + Buffer.from(signer, "hex"), + network.hrp + ); + + const [account] = await wallet.getAccounts(); + + let clientBase: TendermintClient; + + switch (network.tm_version || "37") { + case "34": + clientBase = await Tendermint34Client.connect(network.url); + break; + case "37": + clientBase = await Tendermint37Client.connect(network.url); + break; + } + + const wasm = await SigningCosmWasmClient.createWithSigner( + clientBase, + wallet, + { + gasPrice: GasPrice.fromString(`${network.gas.price}${network.gas.denom}`), + } + ); + const stargate = await SigningStargateClient.createWithSigner( + clientBase, + wallet, + { + gasPrice: GasPrice.fromString(`${network.gas.price}${network.gas.denom}`), + } + ); + + const pubkey = Secp256k1.uncompressPubkey(account.pubkey); + const ethaddr = keccak256(pubkey.slice(1)).slice(-20); + + return { + wasm, + stargate, + signer: account.address, + signer_addr: Buffer.from(ethaddr).toString("hex"), + signer_pubkey: Buffer.from(account.pubkey).toString("hex"), + }; +} diff --git a/scripts/src/contracts/hpl_hook_aggregate.ts b/scripts/src/contracts/hpl_hook_aggregate.ts new file mode 100644 index 00000000..cbbb00b0 --- /dev/null +++ b/scripts/src/contracts/hpl_hook_aggregate.ts @@ -0,0 +1,7 @@ +import { injectable } from "inversify"; +import { BaseContract } from "../types"; + +@injectable() +export class HplHookAggregate extends BaseContract { + contractName: string = "hpl_hook_aggregate"; +} diff --git a/scripts/src/contracts/hpl_hook_merkle.ts b/scripts/src/contracts/hpl_hook_merkle.ts index d216b8e8..81907d85 100644 --- a/scripts/src/contracts/hpl_hook_merkle.ts +++ b/scripts/src/contracts/hpl_hook_merkle.ts @@ -1,5 +1,7 @@ +import { injectable } from "inversify"; import { BaseContract } from "../types"; -export default class HplHookMerkle extends BaseContract { +@injectable() +export class HplHookMerkle extends BaseContract { contractName: string = "hpl_hook_merkle"; } diff --git a/scripts/src/contracts/hpl_hook_pausable.ts b/scripts/src/contracts/hpl_hook_pausable.ts index b32e3269..c6981d64 100644 --- a/scripts/src/contracts/hpl_hook_pausable.ts +++ b/scripts/src/contracts/hpl_hook_pausable.ts @@ -1,5 +1,7 @@ +import { injectable } from "inversify"; import { BaseContract } from "../types"; -export default class HplHookPausable extends BaseContract { +@injectable() +export class HplHookPausable extends BaseContract { contractName: string = "hpl_hook_pausable"; } diff --git a/scripts/src/contracts/hpl_hook_routing.ts b/scripts/src/contracts/hpl_hook_routing.ts index 35bcdd94..4531cbec 100644 --- a/scripts/src/contracts/hpl_hook_routing.ts +++ b/scripts/src/contracts/hpl_hook_routing.ts @@ -1,5 +1,7 @@ +import { injectable } from "inversify"; import { BaseContract } from "../types"; -export default class HplHookRouting extends BaseContract { +@injectable() +export class HplHookRouting extends BaseContract { contractName: string = "hpl_hook_routing"; } diff --git a/scripts/src/contracts/hpl_hook_routing_custom.ts b/scripts/src/contracts/hpl_hook_routing_custom.ts index 56133af4..dc6339be 100644 --- a/scripts/src/contracts/hpl_hook_routing_custom.ts +++ b/scripts/src/contracts/hpl_hook_routing_custom.ts @@ -1,5 +1,7 @@ +import { injectable } from "inversify"; import { BaseContract } from "../types"; -export default class HplHookRoutingCustom extends BaseContract { +@injectable() +export class HplHookRoutingCustom extends BaseContract { contractName: string = "hpl_hook_routing_custom"; } diff --git a/scripts/src/contracts/hpl_hook_routing_fallback.ts b/scripts/src/contracts/hpl_hook_routing_fallback.ts index a6b71741..2b474822 100644 --- a/scripts/src/contracts/hpl_hook_routing_fallback.ts +++ b/scripts/src/contracts/hpl_hook_routing_fallback.ts @@ -1,5 +1,7 @@ +import { injectable } from "inversify"; import { BaseContract } from "../types"; -export default class HplHookRoutingFallback extends BaseContract { +@injectable() +export class HplHookRoutingFallback extends BaseContract { contractName: string = "hpl_hook_routing_fallback"; } diff --git a/scripts/src/contracts/hpl_igp.ts b/scripts/src/contracts/hpl_igp.ts index 857448b8..c0505b82 100644 --- a/scripts/src/contracts/hpl_igp.ts +++ b/scripts/src/contracts/hpl_igp.ts @@ -1,5 +1,7 @@ +import { injectable } from "inversify"; import { BaseContract } from "../types"; -export default class HplIgp extends BaseContract { +@injectable() +export class HplIgp extends BaseContract { contractName: string = "hpl_igp"; } diff --git a/scripts/src/contracts/hpl_igp_oracle.ts b/scripts/src/contracts/hpl_igp_oracle.ts index 771e803c..b4381564 100644 --- a/scripts/src/contracts/hpl_igp_oracle.ts +++ b/scripts/src/contracts/hpl_igp_oracle.ts @@ -1,5 +1,7 @@ +import { injectable } from "inversify"; import { BaseContract } from "../types"; -export default class HplIgpGasOracle extends BaseContract { +@injectable() +export class HplIgpOracle extends BaseContract { contractName: string = "hpl_igp_oracle"; } diff --git a/scripts/src/contracts/hpl_ism_aggregate.ts b/scripts/src/contracts/hpl_ism_aggregate.ts new file mode 100644 index 00000000..45dc95ec --- /dev/null +++ b/scripts/src/contracts/hpl_ism_aggregate.ts @@ -0,0 +1,7 @@ +import { injectable } from "inversify"; +import { BaseContract } from "../types"; + +@injectable() +export class HplIsmAggregate extends BaseContract { + contractName: string = "hpl_ism_aggregate"; +} diff --git a/scripts/src/contracts/hpl_ism_multisig.ts b/scripts/src/contracts/hpl_ism_multisig.ts index b1ea2a95..00a5382a 100644 --- a/scripts/src/contracts/hpl_ism_multisig.ts +++ b/scripts/src/contracts/hpl_ism_multisig.ts @@ -1,5 +1,7 @@ +import { injectable } from "inversify"; import { BaseContract } from "../types"; -export default class HplIsmMultisig extends BaseContract { +@injectable() +export class HplIsmMultisig extends BaseContract { contractName: string = "hpl_ism_multisig"; } diff --git a/scripts/src/contracts/hpl_ism_routing.ts b/scripts/src/contracts/hpl_ism_routing.ts index e61f8c89..2190eb83 100644 --- a/scripts/src/contracts/hpl_ism_routing.ts +++ b/scripts/src/contracts/hpl_ism_routing.ts @@ -1,5 +1,7 @@ +import { injectable } from "inversify"; import { BaseContract } from "../types"; -export default class HplIsmRouting extends BaseContract { +@injectable() +export class HplIsmRouting extends BaseContract { contractName: string = "hpl_ism_routing"; } diff --git a/scripts/src/contracts/hpl_mailbox.ts b/scripts/src/contracts/hpl_mailbox.ts index 9397420a..2dfb9230 100644 --- a/scripts/src/contracts/hpl_mailbox.ts +++ b/scripts/src/contracts/hpl_mailbox.ts @@ -1,5 +1,7 @@ +import { injectable } from "inversify"; import { BaseContract } from "../types"; -export default class HplMailbox extends BaseContract { +@injectable() +export class HplMailbox extends BaseContract { contractName: string = "hpl_mailbox"; } diff --git a/scripts/src/contracts/hpl_test_mock_hook.ts b/scripts/src/contracts/hpl_test_mock_hook.ts index 95eaed3c..a26e26a6 100644 --- a/scripts/src/contracts/hpl_test_mock_hook.ts +++ b/scripts/src/contracts/hpl_test_mock_hook.ts @@ -1,5 +1,7 @@ +import { injectable } from "inversify"; import { BaseContract } from "../types"; -export default class HplTestMockHook extends BaseContract { +@injectable() +export class HplTestMockHook extends BaseContract { contractName: string = "hpl_test_mock_hook"; } diff --git a/scripts/src/contracts/hpl_test_mock_msg_receiver.ts b/scripts/src/contracts/hpl_test_mock_msg_receiver.ts index fc6e38e5..4e84e79a 100644 --- a/scripts/src/contracts/hpl_test_mock_msg_receiver.ts +++ b/scripts/src/contracts/hpl_test_mock_msg_receiver.ts @@ -1,5 +1,7 @@ +import { injectable } from "inversify"; import { BaseContract } from "../types"; -export default class HplTestMockMsgReceiver extends BaseContract { +@injectable() +export class HplTestMockMsgReceiver extends BaseContract { contractName: string = "hpl_test_mock_msg_receiver"; } diff --git a/scripts/src/contracts/hpl_validator_announce.ts b/scripts/src/contracts/hpl_validator_announce.ts index ecccf80a..b887331a 100644 --- a/scripts/src/contracts/hpl_validator_announce.ts +++ b/scripts/src/contracts/hpl_validator_announce.ts @@ -1,5 +1,7 @@ +import { injectable } from "inversify"; import { BaseContract } from "../types"; -export default class HplValidatorAnnounce extends BaseContract { +@injectable() +export class HplValidatorAnnounce extends BaseContract { contractName: string = "hpl_validator_announce"; } diff --git a/scripts/src/contracts/hpl_warp_cw20.ts b/scripts/src/contracts/hpl_warp_cw20.ts index 0b79f9ad..cea3aa9e 100644 --- a/scripts/src/contracts/hpl_warp_cw20.ts +++ b/scripts/src/contracts/hpl_warp_cw20.ts @@ -1,5 +1,7 @@ +import { injectable } from "inversify"; import { BaseContract } from "../types"; -export default class HplWarpCw20 extends BaseContract { +@injectable() +export class HplWarpCw20 extends BaseContract { contractName: string = "hpl_warp_cw20"; } diff --git a/scripts/src/contracts/hpl_warp_native.ts b/scripts/src/contracts/hpl_warp_native.ts index 4fbd546a..36d7d3a1 100644 --- a/scripts/src/contracts/hpl_warp_native.ts +++ b/scripts/src/contracts/hpl_warp_native.ts @@ -1,5 +1,7 @@ +import { injectable } from "inversify"; import { BaseContract } from "../types"; -export default class HplWarpNative extends BaseContract { +@injectable() +export class HplWarpNative extends BaseContract { contractName: string = "hpl_warp_native"; } diff --git a/scripts/src/contracts/index.ts b/scripts/src/contracts/index.ts index 1e996108..8016e991 100644 --- a/scripts/src/contracts/index.ts +++ b/scripts/src/contracts/index.ts @@ -1,3 +1,21 @@ +export * from "./hpl_hook_aggregate"; +export * from "./hpl_hook_merkle"; +export * from "./hpl_hook_pausable"; +export * from "./hpl_hook_routing"; +export * from "./hpl_hook_routing_custom"; +export * from "./hpl_hook_routing_fallback"; +export * from "./hpl_igp"; +export * from "./hpl_igp_oracle"; +export * from "./hpl_ism_aggregate"; +export * from "./hpl_ism_multisig"; +export * from "./hpl_ism_routing"; +export * from "./hpl_mailbox"; +export * from "./hpl_test_mock_hook"; +export * from "./hpl_test_mock_msg_receiver"; +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"; @@ -7,24 +25,43 @@ const contractNames: string[] = readdirSync(__dirname) .filter((f) => f !== "index.ts") .map((f) => f.replace(".ts", "")); -const contractHandlers: { [key: string]: ContractConstructor } = contractNames - .reduce((acc, cur) =>{ - acc[cur] = require(`./${cur}`).default - return acc; - }, {} as { [key: string]: ContractConstructor}); +const contractHandlers: { [key: string]: ContractConstructor } = + contractNames.reduce((acc, cur) => { + const className = cur + .split("_") + .map((v) => v[0].toUpperCase() + v.slice(1)) + .join(""); + acc[cur] = require(`./${cur}`)[className]; + return acc; + }, {} as { [key: string]: ContractConstructor }); export function getTargetContractName(): string[] { return contractNames; } -export function getTargetContract(ctx: Context, client: SigningCosmWasmClient, signer: string, container: Container): { [key: string]: Contract } { +export function getTargetContract( + ctx: Context, + client: SigningCosmWasmClient, + signer: string, + container: Container +): { [key: string]: Contract } { return Object.keys(contractHandlers).reduce((acc, cur) => { const { codeId, digest, address } = ctx.contracts[cur] || {}; - acc[cur] = new contractHandlers[cur](address, codeId, digest, signer, client); + + try { + acc[cur] = new contractHandlers[cur]( + address, + codeId, + digest, + signer, + client + ); + } catch (e) { + throw Error(`Failed to instantiate contract ${cur}: ${e}`); + } container.bind(contractHandlers[cur]).toConstantValue(acc[cur]); return acc; }, {} as { [key: string]: Contract }); } - diff --git a/scripts/src/conv.ts b/scripts/src/conv.ts new file mode 100644 index 00000000..ac1b9488 --- /dev/null +++ b/scripts/src/conv.ts @@ -0,0 +1,4 @@ +export const addPad = (v: string): string => { + const s = v.startsWith("0x") ? v.slice(2) : v; + return s.padStart(64, "0"); +}; diff --git a/scripts/src/deploy.ts b/scripts/src/deploy.ts new file mode 100644 index 00000000..8f176c41 --- /dev/null +++ b/scripts/src/deploy.ts @@ -0,0 +1,151 @@ +import { Client, IsmType } from "../src/config"; +import { + HplMailbox, + HplValidatorAnnounce, + HplHookAggregate, + HplHookMerkle, + HplHookPausable, + HplHookRouting, + HplHookRoutingCustom, + HplIgp, + HplIsmAggregate, + HplIsmMultisig, + HplIsmRouting, + HplTestMockHook, + HplTestMockMsgReceiver, + HplWarpCw20, + HplWarpNative, + HplIgpOracle, +} from "./contracts"; + +export type Contracts = { + core: { + mailbox: HplMailbox; + va: HplValidatorAnnounce; + }; + hooks: { + aggregate: HplHookAggregate; + merkle: HplHookMerkle; + pausable: HplHookPausable; + routing: HplHookRouting; + routing_custom: HplHookRoutingCustom; + routing_fallback: HplHookRoutingCustom; + }; + igp: { + core: HplIgp; + oracle: HplIgpOracle; + }; + isms: { + aggregate: HplIsmAggregate; + multisig: HplIsmMultisig; + routing: HplIsmRouting; + }; + mocks: { + hook: HplTestMockHook; + receiver: HplTestMockMsgReceiver; + }; + warp: { + cw20: HplWarpCw20; + native: HplWarpNative; + }; +}; + +const name = (c: any) => c.contractName; + +export const deploy_ism = async ( + client: Client, + ism: IsmType, + contracts: Contracts +): Promise => { + const { isms } = contracts; + + switch (ism.type) { + case "multisig": + console.log("Instantiate Multisig ISM contract"); + const multisig_ism_res = await isms.multisig.instantiate({ + owner: ism.owner === "" ? client.signer : ism.owner, + }); + + console.log("Enroll validators"); + console.log(ism); + console.log( + Object.entries(ism.validators).flatMap(([domain, validator]) => + validator.addrs.map((v) => ({ + domain: Number(domain), + validator: v, + })) + ) + ); + await client.wasm.execute( + client.signer, + multisig_ism_res.address!, + { + enroll_validators: { + set: Object.entries(ism.validators).flatMap(([domain, validator]) => + validator.addrs.map((v) => ({ + domain: Number(domain), + validator: v, + })) + ), + }, + }, + "auto" + ); + + console.log("Set thresholds"); + await client.wasm.execute( + client.signer, + multisig_ism_res.address!, + { + set_thresholds: { + set: Object.entries(ism.validators).map( + ([domain, { threshold }]) => ({ + domain: Number(domain), + threshold, + }) + ), + }, + }, + "auto" + ); + + return multisig_ism_res.address!; + + case "aggregate": + const aggregate_ism_res = await isms.aggregate.instantiate({ + owner: ism.owner === "" ? client.signer : ism.owner, + isms: await Promise.all( + ism.isms.map((v) => deploy_ism(client, v, contracts)) + ), + }); + + return aggregate_ism_res.address!; + case "routing": + const routing_ism_res = await isms.routing.instantiate({ + owner: ism.owner === "" ? client.signer : ism.owner, + }); + + await client.wasm.execute( + client.signer, + routing_ism_res.address!, + { + router: { + set_routes: { + set: await Promise.all( + Object.entries(ism.isms).map(async ([domain, v]) => { + const route = await deploy_ism(client, v, contracts); + return { domain, route }; + }) + ), + }, + }, + }, + "auto" + ); + + return routing_ism_res.address!; + + default: + throw new Error("invalid ism type"); + } +}; diff --git a/scripts/src/index.ts b/scripts/src/index.ts index 9cf672ba..b3e9b860 100644 --- a/scripts/src/index.ts +++ b/scripts/src/index.ts @@ -1,22 +1,18 @@ +import "reflect-metadata"; + import colors from "colors"; import { loadWasmFileDigest, getWasmPath } from "./load_wasm"; import { loadContext, saveContext } from "./load_context"; import { getTargetContract, getTargetContractName } from "./contracts"; import { CodeUpdate, CodeCreate, Context } from "./types"; import * as readline from "readline"; -import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate"; -import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; -import { GasPrice } from "@cosmjs/stargate"; + import { AxiosError } from "axios"; import { CONTAINER } from "./ioc"; import { runMigrations } from "./migrations"; +import { config, getSigningClient } from "./config"; colors.enable(); -const NETWORK_ID = process.env.NETWORK_ID || "osmo-test-5"; -const NETWORK_HRP = process.env.NETWORK_HRP || "osmo"; -const NETWORK_URL = - process.env.NETWORK_URL || "https://rpc.osmotest5.osmosis.zone"; -const NETWORK_GAS = process.env.NETWORK_GAS || "0.025uosmo"; function askQuestion(query: string) { const rl = readline.createInterface({ @@ -32,36 +28,21 @@ function askQuestion(query: string) { ); } -async function getSigningClient(): Promise<{ - client: SigningCosmWasmClient; - address: string; -}> { - const mnemonic = process.env["SIGNING_MNEMONIC"] as string; - const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { - prefix: NETWORK_HRP, - }); - const [{ address }] = await wallet.getAccounts(); - - const client = await SigningCosmWasmClient.connectWithSigner( - NETWORK_URL, - wallet, - { - gasPrice: GasPrice.fromString(NETWORK_GAS), - } - ); - return { client, address }; -} - async function main() { const digest = await loadWasmFileDigest(); - const context = await loadContext(NETWORK_ID); + const context = loadContext(config.network.id); const targetContractName = getTargetContractName(); - const { client, address } = await getSigningClient(); - context.address = address; + const client = await getSigningClient(config); + context.address = client.signer; CONTAINER.bind(Context).toConstantValue(context); - const contracts = getTargetContract(context, client, address, CONTAINER); + const contracts = getTargetContract( + context, + client.wasm, + client.signer, + CONTAINER + ); console.log("check exist contracts...."); const codeChanges = targetContractName @@ -128,7 +109,7 @@ async function main() { contract.digest = v.digest; const contractContext = await contract.upload(); context.contracts[v.contractName] = contractContext; - saveContext(NETWORK_ID, context); + saveContext(config.network.id, context); console.log("OK".green, "as", contractContext.codeId); } catch (e) { @@ -141,7 +122,7 @@ async function main() { console.log("No contracts to upload."); } - runMigrations(NETWORK_ID, false); + runMigrations(config.network.id, false); } main(); diff --git a/scripts/src/load_wasm.ts b/scripts/src/load_wasm.ts index e269aab5..a1f9cdeb 100644 --- a/scripts/src/load_wasm.ts +++ b/scripts/src/load_wasm.ts @@ -53,5 +53,5 @@ export async function loadWasmFileDigest() { } export function getWasmPath(contractName: string): string { - return path.join(directoryPath, `${contractName}-aarch64.wasm`); + return path.join(directoryPath, `${contractName}.wasm`); } diff --git a/scripts/src/migrations/InitializeStandalone.ts b/scripts/src/migrations/InitializeStandalone.ts new file mode 100644 index 00000000..920cc5b4 --- /dev/null +++ b/scripts/src/migrations/InitializeStandalone.ts @@ -0,0 +1,74 @@ +import { injectable } from "inversify"; +import { Context, Migration } from "../types"; +import { + HplMailbox, + HplHookMerkle, + HplIgpGasOracle, + HplIsmMultisig, + HplTestMockHook, +} from "../contracts"; + +@injectable() +export default class InitializeStandalone implements Migration { + name: string = "initialize_standalone"; + after: string = ""; + + constructor( + private ctx: Context, + private mailbox: HplMailbox, + private hook_merkle: HplHookMerkle, + private igp: HplIgp, + private igp_oracle: HplIgpGasOracle, + private ism_multisig: HplIsmMultisig, + private test_mock_hook: HplTestMockHook + ) {} + + run = async (): Promise => { + // init mailbox + this.ctx.contracts[this.mailbox.contractName] = + await this.mailbox.instantiate({ + hrp: "dual", + owner: this.ctx.address!, + domain: 33333, + }); + + // init merkle hook - (required hook) + this.ctx.contracts[this.hook_merkle.contractName] = + await this.hook_merkle.instantiate({ + owner: this.ctx.address!, + mailbox: this.ctx.contracts[this.mailbox.contractName].address, + }); + + // init mock hook - (default hook) + this.ctx.contracts[this.test_mock_hook.contractName] = + await this.test_mock_hook.instantiate({}); + + // init igp oracle + this.ctx.contracts[this.igp_oracle.contractName] = + await this.igp_oracle.instantiate({ + owner: this.ctx.address!, + }); + + // init igp + this.ctx.contracts[this.igp.contractName] = await this.igp.instantiate({ + hrp: "dual", + owner: this.ctx.address!, + mailbox: this.ctx.contracts[this.mailbox.contractName].address, + gas_token: "token", + beneficiary: this.ctx.address!, + }); + + // init ism multisig + this.ctx.contracts[this.ism_multisig.contractName] = + await this.ism_multisig.instantiate({ + hrp: "dual", + owner: this.ctx.address!, + }); + + return this.ctx; + }; + + setContext = (ctx: Context) => { + this.ctx = ctx; + }; +} diff --git a/scripts/src/migrations/index.ts b/scripts/src/migrations/index.ts index adb2161a..b298a1bb 100644 --- a/scripts/src/migrations/index.ts +++ b/scripts/src/migrations/index.ts @@ -1,17 +1,15 @@ import { readdirSync } from "fs"; import { CONTAINER } from "../ioc"; -import { Context, Migration } from "../types"; -import 'reflect-metadata' +import { Context } from "../types"; import { saveContext } from "../load_context"; - -const MIGRATIONS: {[key: string]: any } = readdirSync(__dirname) +const MIGRATIONS: { [key: string]: any } = readdirSync(__dirname) .filter((f) => f !== "index.ts") .map((f) => f.replace(".ts", "")) - .reduce((acc, cur) =>{ - acc[cur] = require(`./${cur}`).default + .reduce((acc, cur) => { + acc[cur] = require(`./${cur}`).default; return acc; - }, {} as {[key: string]: any}); + }, {} as { [key: string]: any }); export async function runMigrations(network: string, dryRun: boolean) { const migraiotnMap: { [key: string]: any } = {}; @@ -26,28 +24,33 @@ export async function runMigrations(network: string, dryRun: boolean) { migraiotnMap[obj.name] = obj; }); - // find initials - const availableInitials = Object.keys(migraiotnMap).map((key) => migraiotnMap[key]).filter((obj) => obj.after === ""); - if(availableInitials.length != 1) { + const availableInitials = Object.keys(migraiotnMap) + .map((key) => migraiotnMap[key]) + .filter((obj) => obj.after === ""); + if (availableInitials.length != 1) { throw new Error("There must be one initial migration"); } let current = availableInitials[0]; let migrationOrder = [current]; - while(true) { + while (true) { let next: any | undefined = undefined; Object.keys(migraiotnMap).forEach((key) => { const obj = migraiotnMap[key]; - if(obj.after === current.name) { - (next === undefined) ? (next = obj) : (() => { throw new Error("There must be one next migration") })(); + if (obj.after === current.name) { + next === undefined + ? (next = obj) + : (() => { + throw new Error("There must be one next migration"); + })(); } }); - if(next === undefined) break; + if (next === undefined) break; migrationOrder.push(next); current = next; @@ -55,7 +58,8 @@ export async function runMigrations(network: string, dryRun: boolean) { // get contexts and check after; let ctx = CONTAINER.get(Context); - const migStartAt = migrationOrder.findIndex((obj) => obj.name === ctx.latestMigration) + 1; + const migStartAt = + migrationOrder.findIndex((obj) => obj.name === ctx.latestMigration) + 1; // migrate if (migrationOrder.slice(migStartAt).length === 0) { @@ -63,9 +67,9 @@ export async function runMigrations(network: string, dryRun: boolean) { return; } - console.log("\nrun migrations...\n") + console.log("\nrun migrations...\n"); - for(let obj of migrationOrder.slice(migStartAt)) { + for (let obj of migrationOrder.slice(migStartAt)) { process.stdout.write("[MIGRATION]".gray); process.stdout.write(` ${obj.name} ... `); @@ -82,21 +86,19 @@ export async function runMigrations(network: string, dryRun: boolean) { saveContext(network, ctx); console.log("OK".green); - } catch(err) { + } catch (err) { console.log("FAIL".red); throw err; } } console.log("\n[INFO]".gray, "All migrations are done."); - console.log("\n============= Migration Result =============\n") + console.log("\n============= Migration Result =============\n"); Object.keys(ctx.contracts).forEach((key) => { const contract = ctx.contracts[key]; - console.log(`${key}`.padEnd(30), '=>', `${contract.address}`); + console.log(`${key}`.padEnd(30), "=>", `${contract.address}`); }); } -export async function runContract(network: string) { - -} +export async function runContract(network: string) {} diff --git a/scripts/src/migrations/initialize.ts b/scripts/src/migrations/initialize.ts index 9dfe6eab..0e1772a7 100644 --- a/scripts/src/migrations/initialize.ts +++ b/scripts/src/migrations/initialize.ts @@ -1,11 +1,13 @@ import { injectable } from "inversify"; import { Context, Migration } from "../types"; -import HplMailbox from "../contracts/hpl_mailbox"; -import HplIgpGasOracle from "../contracts/hpl_igp_oracle"; -import HplIgpCore from "../contracts/hpl_igp"; -import HplIsmMultisig from "../contracts/hpl_ism_multisig"; -import HplHookMerkle from "../contracts/hpl_hook_merkle"; -import HplTestMockHook from "../contracts/hpl_test_mock_hook"; +import { + HplMailbox, + HplHookMerkle, + HplIgpOracle, + HplIsmMultisig, + HplTestMockHook, + HplIgp, +} from "../contracts"; @injectable() export default class InitializeStandalone implements Migration { @@ -16,8 +18,8 @@ export default class InitializeStandalone implements Migration { private ctx: Context, private mailbox: HplMailbox, private hook_merkle: HplHookMerkle, - private igp: HplIgpCore, - private igp_oracle: HplIgpGasOracle, + private igp: HplIgp, + private igp_oracle: HplIgpOracle, private ism_multisig: HplIsmMultisig, private test_mock_hook: HplTestMockHook ) {} @@ -44,7 +46,9 @@ export default class InitializeStandalone implements Migration { // init igp oracle this.ctx.contracts[this.igp_oracle.contractName] = - await this.igp_oracle.instantiate({}); + await this.igp_oracle.instantiate({ + owner: this.ctx.address!, + }); // init igp this.ctx.contracts[this.igp.contractName] = await this.igp.instantiate({ @@ -52,7 +56,7 @@ export default class InitializeStandalone implements Migration { owner: this.ctx.address!, mailbox: this.ctx.contracts[this.mailbox.contractName].address, gas_token: "token", - beneficairy: this.ctx.address!, + beneficiary: this.ctx.address!, }); // init ism multisig diff --git a/scripts/src/migrations/mailbox.ts b/scripts/src/migrations/mailbox.ts index 1016733c..aa79c3d2 100644 --- a/scripts/src/migrations/mailbox.ts +++ b/scripts/src/migrations/mailbox.ts @@ -1,8 +1,6 @@ import { injectable } from "inversify"; import { Context, HplMailboxInstantiateMsg, Migration } from "../types"; -import HplMailbox from "../contracts/hpl_mailbox"; -import HplIsmMultisig from "../contracts/hpl_ism_multisig"; - +import { HplIsmMultisig, HplMailbox } from "../contracts"; @injectable() export default class MailboxMigration implements Migration { @@ -12,17 +10,21 @@ export default class MailboxMigration implements Migration { constructor( private ctx: Context, private mailbox: HplMailbox, - private ism_multisig: HplIsmMultisig, + private ism_multisig: HplIsmMultisig ) {} run = async (): Promise => { const mailboxInit: HplMailboxInstantiateMsg = { owner: this.ctx.address!, - default_ism: this.ism_multisig.address!, - } - this.ctx.contracts[this.mailbox.contractName] = await this.mailbox.instantiate(mailboxInit); + hrp: "dual", + domain: 33333, + }; + this.ctx.contracts[this.mailbox.contractName] = + await this.mailbox.instantiate(mailboxInit); return this.ctx; - } + }; - setContext = (ctx: Context) => { this.ctx = ctx } + setContext = (ctx: Context) => { + this.ctx = ctx; + }; } diff --git a/scripts/src/migrations/mailbox_related.ts b/scripts/src/migrations/mailbox_related.ts index 7b2d61cc..dd9b3aeb 100644 --- a/scripts/src/migrations/mailbox_related.ts +++ b/scripts/src/migrations/mailbox_related.ts @@ -1,14 +1,6 @@ import { injectable } from "inversify"; -import { - Context, - HplIsmRoutingInstantiateMsg, - HplMulticallInstantiateMsg, - HplValidatorAnnounceInstantiateMsg, - Migration, -} from "../types"; -import HplMailbox from "../contracts/hpl_mailbox"; -import HplIsmRouting from "../contracts/hpl_ism_routing"; -import HplValidatorAnnounce from "../contracts/hpl_validator_announce"; +import { Context, Migration } from "../types"; +import { HplIsmRouting, HplMailbox, HplValidatorAnnounce } from "../contracts"; @injectable() export default class MailboxMigration implements Migration { @@ -23,24 +15,24 @@ export default class MailboxMigration implements Migration { ) {} run = async (): Promise => { - const routingMsgs: HplIsmRoutingInstantiateMsg = { - owner: this.ctx.address!, - isms: [ - { - domain: 4337, - address: this.mailbox.address!, - }, - ], - }; - this.ctx.contracts[this.ismRouting.contractName] = - await this.ismRouting.instantiate(routingMsgs); + // const routingMsgs: HplIsmRoutingInstantiateMsg = { + // owner: this.ctx.address!, + // isms: [ + // { + // domain: 4337, + // address: this.mailbox.address!, + // }, + // ], + // }; + // this.ctx.contracts[this.ismRouting.contractName] = + // await this.ismRouting.instantiate(routingMsgs); - const vaMsg: HplValidatorAnnounceInstantiateMsg = { - addr_prefix: "osmo", - mailbox: this.mailbox.address!, - local_domain: 4337, - }; - this.ctx.contracts[this.va.contractName] = await this.va.instantiate(vaMsg); + // const vaMsg: HplValidatorAnnounceInstantiateMsg = { + // addr_prefix: "osmo", + // mailbox: this.mailbox.address!, + // local_domain: 4337, + // }; + // this.ctx.contracts[this.va.contractName] = await this.va.instantiate(vaMsg); return this.ctx; }; diff --git a/scripts/src/types.ts b/scripts/src/types.ts index 811ad22b..b28f9edc 100644 --- a/scripts/src/types.ts +++ b/scripts/src/types.ts @@ -1,6 +1,10 @@ -import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate"; +import { + ExecuteResult, + SigningCosmWasmClient, +} from "@cosmjs/cosmwasm-stargate"; import { getWasmPath } from "./load_wasm"; import fs from "fs"; +import { fromBech32 } from "@cosmjs/encoding"; export interface ContractContext { codeId: number | undefined; @@ -46,25 +50,13 @@ export interface Contract { export abstract class BaseContract implements Contract { contractName: string; - address: string | undefined; - codeId: number | undefined; - digest: string; - client: SigningCosmWasmClient; - signer: string; - constructor( - address: string | undefined, - codeId: number | undefined, - digest: string, - signer: string, - client: SigningCosmWasmClient - ) { - this.address = address; - this.client = client; - this.digest = digest; - this.codeId = codeId; - this.signer = signer; - } + public address: string | undefined, + public codeId: number | undefined, + public digest: string, + public signer: string, + public client: SigningCosmWasmClient + ) {} public getContractContext(): ContractContext { return { @@ -83,19 +75,65 @@ export abstract class BaseContract implements Contract { } public async instantiate(msg: any): Promise { - const instantiateMsg = msg as HplMailboxInstantiateMsg; const contract = await this.client.instantiate( this.signer, this.codeId!, - instantiateMsg, + msg, this.contractName, "auto", { admin: this.signer } ); + console.log( + [ + this.contractName.padEnd(30), + contract.contractAddress.padEnd(65), + Buffer.from(fromBech32(contract.contractAddress).data) + .toString("hex") + .padEnd(65), + contract.transactionHash.padEnd(65), + ].join("| ") + ); + this.address = contract.contractAddress; return this.getContractContext(); } + + // overloads + public async execute(msg: any): Promise; + public async execute( + msg: any, + funds: { denom: string; amount: string }[] + ): Promise; + + // implementation + public async execute( + msg: any, + funds?: { denom: string; amount: string }[] + ): Promise { + const res = await this.client.execute( + this.signer, + this.address!, + msg, + "auto", + undefined, + funds + ); + console.log( + [ + `${this.contractName}:${Object.keys(msg)[0]}`.padEnd(30), + res.transactionHash.padEnd(65), + ].join("| ") + ); + + return res; + } + + public async query(msg: any): Promise { + const res = await this.client.queryContractSmart(this.address!, msg); + + return res; + } } export interface ContractConstructor { @@ -144,7 +182,8 @@ export interface HplIsmRoutingInstantiateMsg { export interface HplMailboxInstantiateMsg { owner: string; - default_ism: string; + hrp: string; + domain: number; } export interface HplMulticallInstantiateMsg { diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json index 0f802280..25615870 100644 --- a/scripts/tsconfig.json +++ b/scripts/tsconfig.json @@ -1,9 +1,6 @@ { "compilerOptions": { - "lib": [ - "es2021", - "webworker" - ], + "lib": ["es2021", "webworker"], "module": "commonjs", "target": "es2021", "strict": true, @@ -16,8 +13,7 @@ "emitDecoratorMetadata": true, "strictPropertyInitialization": false, "outDir": "dist", - "declaration": true /* Skip type checking all .d.ts files. */ + "declaration": true /* Skip type checking all .d.ts files. */ }, - "include": [ "src/**/*.ts", "*.ts" ], + "include": ["src/**/*.ts", "*.ts", "action/**/*.ts"] } - diff --git a/scripts/warp.ts b/scripts/warp.ts deleted file mode 100644 index 51c9c31b..00000000 --- a/scripts/warp.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate"; -import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; -import { GasPrice } from "@cosmjs/stargate"; - -import { loadContext } from "./src/load_context"; -import HplMailbox from "./src/contracts/hpl_mailbox"; -import { Context } from "./src/types"; -import HplWarpNative from "./src/contracts/hpl_warp_native"; - -const NETWORK_ID = process.env.NETWORK_ID || "osmo-test-5"; -const NETWORK_HRP = process.env.NETWORK_HRP || "osmo"; -const NETWORK_URL = - process.env.NETWORK_URL || "https://rpc.osmotest5.osmosis.zone"; -const NETWORK_GAS = process.env.NETWORK_GAS || "0.025uosmo"; - -async function getSigningClient(): Promise<{ - client: SigningCosmWasmClient; - address: string; -}> { - const mnemonic = process.env["SIGNING_MNEMONIC"] as string; - const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { - prefix: NETWORK_HRP, - }); - const [{ address }] = await wallet.getAccounts(); - - const client = await SigningCosmWasmClient.connectWithSigner( - NETWORK_URL, - wallet, - { - gasPrice: GasPrice.fromString(NETWORK_GAS), - } - ); - return { client, address }; -} - -type Const = new ( - address: string | undefined, - codeId: number | undefined, - digest: string, - signer: string, - client: SigningCosmWasmClient -) => T; - -class ContractFetcher { - constructor( - private ctx: Context, - private owner: string, - private client: SigningCosmWasmClient - ) {} - - public get(f: Const, name: string): T { - return new f( - this.ctx.contracts[name].address, - this.ctx.contracts[name].codeId, - this.ctx.contracts[name].digest, - this.owner, - this.client - ); - } -} - -async function main() { - const { client, address: owner } = await getSigningClient(); - - const ctx = loadContext(NETWORK_ID); - - const fetcher = new ContractFetcher(ctx, owner, client); - - const mailbox = fetcher.get(HplMailbox, "hpl_mailbox"); - - const warp_native = fetcher.get(HplWarpNative, "hpl_warp_native"); - - const target_denom = - "ibc/B5CB286F69D48B2C4F6F8D8CF59011C40590DCF8A91617A5FBA9FF0A7B21307F"; - - const ibc_route = await warp_native.instantiate({ - token: { - collateral: { - denom: target_denom, - }, - }, - hrp: "dual", - owner, - mailbox: mailbox.address!, - }); - - console.log("ibc_route", ibc_route); -} - -main().catch(console.error);