diff --git a/cosmwasm/Cargo.lock b/cosmwasm/Cargo.lock index 2c3fc20235..1a4f2f1567 100644 --- a/cosmwasm/Cargo.lock +++ b/cosmwasm/Cargo.lock @@ -2597,8 +2597,11 @@ dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", + "cw-multi-test", "cw-storage-plus 0.13.4", "semver", + "serde", + "serde-json-wasm 0.4.1", "serde_wormhole", "thiserror", "wormhole-bindings", diff --git a/cosmwasm/contracts/wormchain-ibc-receiver/Cargo.toml b/cosmwasm/contracts/wormchain-ibc-receiver/Cargo.toml index 778fe83edb..58c6be3054 100644 --- a/cosmwasm/contracts/wormchain-ibc-receiver/Cargo.toml +++ b/cosmwasm/contracts/wormchain-ibc-receiver/Cargo.toml @@ -21,3 +21,9 @@ thiserror = "1.0.31" wormhole-bindings = "0.1.0" wormhole-sdk = { workspace = true, features = ["schemars"] } serde_wormhole.workspace = true + +[dev-dependencies] +cw-multi-test = "0.13.2" +serde-json-wasm = "0.4" +wormhole-bindings = { version = "0.1.0", features=["fake"] } +serde = { version = "1.0.137", default-features = false, features = ["derive"] } \ No newline at end of file diff --git a/cosmwasm/contracts/wormchain-ibc-receiver/src/lib.rs b/cosmwasm/contracts/wormchain-ibc-receiver/src/lib.rs index 10d266d8f4..31d3febfdc 100644 --- a/cosmwasm/contracts/wormchain-ibc-receiver/src/lib.rs +++ b/cosmwasm/contracts/wormchain-ibc-receiver/src/lib.rs @@ -3,3 +3,6 @@ pub mod error; pub mod ibc; pub mod msg; pub mod state; + +#[cfg(test)] +pub mod tests; diff --git a/cosmwasm/contracts/wormchain-ibc-receiver/src/tests/integration_tests.rs b/cosmwasm/contracts/wormchain-ibc-receiver/src/tests/integration_tests.rs new file mode 100644 index 0000000000..86210bfe20 --- /dev/null +++ b/cosmwasm/contracts/wormchain-ibc-receiver/src/tests/integration_tests.rs @@ -0,0 +1,362 @@ +use crate::{ + contract::{execute, query}, + msg::{AllChannelChainsResponse, ExecuteMsg, QueryMsg}, + tests::test_utils::{create_gov_vaa_body, create_transfer_vaa_body, sign_vaa_body}, +}; +use anyhow::Error; +use cosmwasm_std::{ + from_binary, + testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, + to_binary, Binary, ContractResult, Deps, DepsMut, Empty, QuerierWrapper, SystemResult, +}; +use wormhole_bindings::{fake::WormholeKeeper, WormholeQuery}; +use wormhole_sdk::{ + ibc_receiver::{Action, GovernancePacket}, + vaa::Body, + Chain, GOVERNANCE_EMITTER, +}; + +#[test] +pub fn add_channel_chain_happy_path() -> anyhow::Result<(), Error> { + let wh = WormholeKeeper::new(); + + let querier: MockQuerier = + MockQuerier::new(&[]).with_custom_handler(|q| match q { + WormholeQuery::VerifyVaa { vaa } => { + match WormholeKeeper::new().verify_vaa(&vaa.0, 0u64) { + Ok(_) => SystemResult::Ok(if let Ok(data) = to_binary(&Empty {}) { + ContractResult::Ok(data) + } else { + ContractResult::Err("Unable to convert to binary".to_string()) + }), + Err(e) => SystemResult::Ok(ContractResult::Err(e.to_string())), + } + } + _ => cosmwasm_std::SystemResult::Ok(cosmwasm_std::ContractResult::Ok( + to_binary(&Empty {}).unwrap(), + )), + }); + + let mut mut_deps = DepsMut { + storage: &mut MockStorage::default(), + api: &MockApi::default(), + querier: QuerierWrapper::new(&querier), + }; + let info = mock_info("sender", &[]); + let env = mock_env(); + + let add_sei_channel_body = create_gov_vaa_body(1, Chain::Sei, *b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00channel-0"); + let (_, add_sei_vaa_binary) = sign_vaa_body(wh.clone(), add_sei_channel_body); + + let submissions = execute( + mut_deps.branch(), + env.clone(), + info.clone(), + ExecuteMsg::SubmitUpdateChannelChain { + vaas: vec![add_sei_vaa_binary], + }, + ); + + assert!( + submissions.is_ok(), + "A proper UpdateChannelChain gov vaa should be accepted" + ); + + // create a readonly deps to use for querying the state + let empty_mock_querier = MockQuerier::::new(&[]); + let readonly_deps = Deps { + storage: mut_deps.storage, + api: mut_deps.api, + querier: QuerierWrapper::new(&empty_mock_querier), + }; + + let channel_binary = query(readonly_deps, env, QueryMsg::AllChannelChains {})?; + let channel: AllChannelChainsResponse = from_binary(&channel_binary)?; + + assert_eq!(channel.channels_chains.len(), 1); + let channel_entry = channel.channels_chains.first().unwrap(); + assert_eq!( + channel_entry.0, + Binary::from(*b"channel-0"), + "the stored channel for sei should initially be channel-0" + ); + assert_eq!( + channel_entry.1, + Into::::into(Chain::Sei), + "the stored channel should be for sei's chain id" + ); + + Ok(()) +} + +#[test] +pub fn add_channel_chain_happy_path_multiple() -> anyhow::Result<(), Error> { + let wh = WormholeKeeper::new(); + + let querier: MockQuerier = + MockQuerier::new(&[]).with_custom_handler(|q| match q { + WormholeQuery::VerifyVaa { vaa } => { + match WormholeKeeper::new().verify_vaa(&vaa.0, 0u64) { + Ok(_) => SystemResult::Ok(if let Ok(data) = to_binary(&Empty {}) { + ContractResult::Ok(data) + } else { + ContractResult::Err("Unable to convert to binary".to_string()) + }), + Err(e) => SystemResult::Ok(ContractResult::Err(e.to_string())), + } + } + _ => cosmwasm_std::SystemResult::Ok(cosmwasm_std::ContractResult::Ok( + to_binary(&Empty {}).unwrap(), + )), + }); + + let mut mut_deps = DepsMut { + storage: &mut MockStorage::default(), + api: &MockApi::default(), + querier: QuerierWrapper::new(&querier), + }; + let info = mock_info("sender", &[]); + + let add_inj_channel_body = create_gov_vaa_body(2, Chain::Injective, *b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00channel-1"); + let (_, add_inj_vaa_bin) = sign_vaa_body(wh.clone(), add_inj_channel_body); + let add_sei_channel_body = create_gov_vaa_body(3, Chain::Sei, *b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00channel-2"); + let (_, add_sei_vaa_binary) = sign_vaa_body(wh.clone(), add_sei_channel_body); + + // add a channel for injective and update the channel set for sei + let submissions = execute( + mut_deps.branch(), + mock_env(), + info.clone(), + ExecuteMsg::SubmitUpdateChannelChain { + vaas: vec![add_sei_vaa_binary, add_inj_vaa_bin], + }, + ); + + assert!( + submissions.is_ok(), + "A pair of proper UpdateChannelChain gov vaas should be accepted" + ); + + // create a readonly deps to use for querying the state + let empty_mock_querier = MockQuerier::::new(&[]); + let readonly_deps = Deps { + storage: mut_deps.storage, + api: mut_deps.api, + querier: QuerierWrapper::new(&empty_mock_querier), + }; + + // refetch all the channels that are in state + let channel_binary = query(readonly_deps, mock_env(), QueryMsg::AllChannelChains {})?; + let AllChannelChainsResponse { + channels_chains: mut channels, + }: AllChannelChainsResponse = from_binary(&channel_binary)?; + + channels.sort_by(|(_, a_chain_id), (_, b_chain_id)| a_chain_id.cmp(b_chain_id)); + + assert_eq!(channels.len(), 2); + + let channel_entry = channels.first().unwrap(); + assert_eq!( + channel_entry.0, + Binary::from(*b"channel-1"), + "the stored channel should be channel-1 " + ); + assert_eq!( + channel_entry.1, + Into::::into(Chain::Injective), + "the stored channel should be for injective's chain id" + ); + + let channel_entry = channels.last().unwrap(); + assert_eq!( + channel_entry.0, + Binary::from(*b"channel-2"), + "the stored channel should be channel-2" + ); + assert_eq!( + channel_entry.1, + Into::::into(Chain::Sei), + "the stored channel should be for sei's chain id" + ); + + Ok(()) +} + +#[test] +pub fn reject_invalid_add_channel_chain_vaas() { + let wh = WormholeKeeper::new(); + + let querier: MockQuerier = + MockQuerier::new(&[]).with_custom_handler(|q| match q { + WormholeQuery::VerifyVaa { vaa } => { + match WormholeKeeper::new().verify_vaa(&vaa.0, 0u64) { + Ok(_) => SystemResult::Ok(if let Ok(data) = to_binary(&Empty {}) { + ContractResult::Ok(data) + } else { + ContractResult::Err("Unable to convert to binary".to_string()) + }), + Err(e) => SystemResult::Ok(ContractResult::Err(e.to_string())), + } + } + _ => cosmwasm_std::SystemResult::Ok(cosmwasm_std::ContractResult::Ok( + to_binary(&Empty {}).unwrap(), + )), + }); + + let mut mut_deps = DepsMut { + storage: &mut MockStorage::default(), + api: &MockApi::default(), + querier: QuerierWrapper::new(&querier), + }; + let info = mock_info("sender", &[]); + + let add_channel_body = create_gov_vaa_body(1, Chain::Wormchain, *b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00channel-0"); + let (_, add_vaa_binary) = sign_vaa_body(wh.clone(), add_channel_body); + + let submissions = execute( + mut_deps.branch(), + mock_env(), + info.clone(), + ExecuteMsg::SubmitUpdateChannelChain { + vaas: vec![add_vaa_binary], + }, + ); + + assert!( + submissions.is_err(), + "Cannot add a channel from Gateway to Gateway" + ); + + let submissions = execute( + mut_deps.branch(), + mock_env(), + info.clone(), + ExecuteMsg::SubmitUpdateChannelChain { + vaas: vec![Binary::from(vec![0u8; 32])], + }, + ); + + assert!( + submissions.is_err(), + "VAA should be rejected if it cannot be parsed because it's too short" + ); + + let add_channel_body = create_transfer_vaa_body(1); + let (_, add_vaa_binary) = sign_vaa_body(wh.clone(), add_channel_body); + + let submissions = execute( + mut_deps.branch(), + mock_env(), + info.clone(), + ExecuteMsg::SubmitUpdateChannelChain { + vaas: vec![add_vaa_binary], + }, + ); + + assert!(submissions.is_err(), "Can only execute governance vaas"); + + let add_channel_body = create_gov_vaa_body(1, Chain::Osmosis, *b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00channel-0"); + let (_, add_vaa_binary) = sign_vaa_body(wh.clone(), add_channel_body); + + let submissions = execute( + mut_deps.branch(), + mock_env(), + info.clone(), + ExecuteMsg::SubmitUpdateChannelChain { + vaas: vec![add_vaa_binary], + }, + ); + + assert!( + submissions.is_ok(), + "Can add a channel from Osmosis to Gateway" + ); + + let add_channel_body: Body = Body { + timestamp: 1u32, + nonce: 1u32, + emitter_chain: Chain::Solana, + emitter_address: GOVERNANCE_EMITTER, + sequence: 1u64, + consistency_level: 0, + payload: GovernancePacket { + chain: Chain::Osmosis, + action: Action::UpdateChannelChain { + channel_id: *b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00channel-0", + chain_id: Chain::CosmosHub, + }, + }, + }; + let (_, add_vaa_binary) = sign_vaa_body(wh.clone(), add_channel_body); + + let submissions = execute( + mut_deps.branch(), + mock_env(), + info.clone(), + ExecuteMsg::SubmitUpdateChannelChain { + vaas: vec![add_vaa_binary], + }, + ); + + assert!( + submissions.is_err(), + "Cannot add a update a chain besides Gateway" + ); +} + +#[test] +pub fn reject_replayed_add_channel_chain_vaas() { + let wh = WormholeKeeper::new(); + + let querier: MockQuerier = + MockQuerier::new(&[]).with_custom_handler(|q| match q { + WormholeQuery::VerifyVaa { vaa } => { + match WormholeKeeper::new().verify_vaa(&vaa.0, 0u64) { + Ok(_) => SystemResult::Ok(if let Ok(data) = to_binary(&Empty {}) { + ContractResult::Ok(data) + } else { + ContractResult::Err("Unable to convert to binary".to_string()) + }), + Err(e) => SystemResult::Ok(ContractResult::Err(e.to_string())), + } + } + _ => cosmwasm_std::SystemResult::Ok(cosmwasm_std::ContractResult::Ok( + to_binary(&Empty {}).unwrap(), + )), + }); + + let mut mut_deps = DepsMut { + storage: &mut MockStorage::default(), + api: &MockApi::default(), + querier: QuerierWrapper::new(&querier), + }; + let info = mock_info("sender", &[]); + + let add_channel_body = create_gov_vaa_body(1, Chain::Osmosis, *b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00channel-0"); + let (_, add_vaa_binary) = sign_vaa_body(wh.clone(), add_channel_body); + + let submissions = execute( + mut_deps.branch(), + mock_env(), + info.clone(), + ExecuteMsg::SubmitUpdateChannelChain { + vaas: vec![add_vaa_binary.clone()], + }, + ); + + assert!( + submissions.is_ok(), + "Can add a channel from Osmosis to Gateway" + ); + + let submissions = execute( + mut_deps.branch(), + mock_env(), + info.clone(), + ExecuteMsg::SubmitUpdateChannelChain { + vaas: vec![add_vaa_binary], + }, + ); + + assert!(submissions.is_err(), "Cannot replay the same VAA"); +} diff --git a/cosmwasm/contracts/wormchain-ibc-receiver/src/tests/mod.rs b/cosmwasm/contracts/wormchain-ibc-receiver/src/tests/mod.rs new file mode 100644 index 0000000000..7e5f6c1060 --- /dev/null +++ b/cosmwasm/contracts/wormchain-ibc-receiver/src/tests/mod.rs @@ -0,0 +1,2 @@ +pub mod integration_tests; +pub mod test_utils; diff --git a/cosmwasm/contracts/wormchain-ibc-receiver/src/tests/test_utils.rs b/cosmwasm/contracts/wormchain-ibc-receiver/src/tests/test_utils.rs new file mode 100644 index 0000000000..702d83bf7c --- /dev/null +++ b/cosmwasm/contracts/wormchain-ibc-receiver/src/tests/test_utils.rs @@ -0,0 +1,66 @@ +use cosmwasm_std::{Binary, Uint256}; +use serde::Serialize; +use wormhole_bindings::fake::WormholeKeeper; +use wormhole_sdk::{ + ibc_receiver::{Action, GovernancePacket}, + token::Message, + vaa::{Body, Header, Vaa}, + Address, Amount, Chain, GOVERNANCE_EMITTER, +}; + +pub fn create_transfer_vaa_body(i: usize) -> Body { + Body { + timestamp: i as u32, + nonce: i as u32, + emitter_chain: (i as u16).into(), + emitter_address: Address([(i as u8); 32]), + sequence: i as u64, + consistency_level: 32, + payload: Message::Transfer { + amount: Amount(Uint256::from(i as u128).to_be_bytes()), + token_address: Address([(i + 1) as u8; 32]), + token_chain: (i as u16).into(), + recipient: Address([i as u8; 32]), + recipient_chain: ((i + 2) as u16).into(), + fee: Amount([0u8; 32]), + }, + } +} + +pub fn create_gov_vaa_body( + i: usize, + chain_id: Chain, + channel_id: [u8; 64], +) -> Body { + Body { + timestamp: i as u32, + nonce: i as u32, + emitter_chain: Chain::Solana, + emitter_address: GOVERNANCE_EMITTER, + sequence: i as u64, + consistency_level: 0, + payload: GovernancePacket { + chain: Chain::Wormchain, + action: Action::UpdateChannelChain { + channel_id, + chain_id, + }, + }, + } +} + +pub fn sign_vaa_body(wh: WormholeKeeper, body: Body

) -> (Vaa

, Binary) { + let data = serde_wormhole::to_vec(&body).unwrap(); + let signatures = WormholeKeeper::new().sign(&data); + + let header = Header { + version: 1, + guardian_set_index: wh.guardian_set_index(), + signatures, + }; + + let v = (header, body).into(); + let data = serde_wormhole::to_vec(&v).map(From::from).unwrap(); + + (v, data) +} diff --git a/cspell-custom-words.txt b/cspell-custom-words.txt index 1cb8060b6f..c8410ef40a 100644 --- a/cspell-custom-words.txt +++ b/cspell-custom-words.txt @@ -1,5 +1,5 @@ -acala Acala +acala Acks Alertmanager algod @@ -12,8 +12,8 @@ authorisation authorise authorised backdoors -bech Bech +bech behaviour Berachain bigset @@ -25,8 +25,8 @@ bytecodes callstack ccqlistener CCTP -celestia Celestia +celestia celo certusone Chainlink @@ -37,8 +37,8 @@ Concat conftest Cosm cosmoshub -cosmwasm Cosmwasm +cosmwasm counterparty cpus crosschain @@ -46,8 +46,8 @@ Cyfrin datagram denoms devnet -dymension Dymension +dymension ethcrypto ethersproject ETHRPC @@ -59,16 +59,16 @@ funder gogoproto goimports gossipv -guardiand GUARDIAND +guardiand guardiand's Hacken hashdump -healthcheck Healthcheck +healthcheck hexdump -holesky Holesky +holesky horcrux ICCO incentivized @@ -76,28 +76,28 @@ incentivizing initialisation initialised initialiser -injective Injective +injective inotify intcblock -ints Ints +ints IPFS journalctl -karura Karura +karura Keccak -kevm KEVM +kevm keymap keytool -klaytn Klaytn +klaytn kompiled kompiles Kudelski -kujira Kujira +kujira lamports lastrun libp @@ -113,11 +113,12 @@ moonscan moretags Neodyme nhooyr -obsv +Nygard Obsv +obsv optimisation -optin Optin +optin parachain pdas permissioned @@ -128,16 +129,17 @@ Polkadot Positionals prefunded promauto -proto Proto +proto protobuf protos prototxt +Pryce's pubkey pushbytes pushint -pytest Pytest +pytest pythnet QUIC ramfs @@ -155,26 +157,29 @@ seievm Sepolia serde setcap -snaxchain SnaxChain -solana +snaxchain Solana +solana Solana's spydk Starport statesync +Strangelove struct structs subdenom -subdenoms Subdenoms +subdenoms +submessage supermajority superminority -tendermint Tendermint +tendermint terrad tokenbridge tokenfactory +toolset trustlessly tsig tsproto @@ -196,14 +201,15 @@ vimdiff vphash wasmhooks wasms +wasmvm WORKDIR -wormchain Wormchain +wormchain wormchaind Wormholescan wormscan wormscanurl xlayer -xpla XPLA -Zellic +xpla +Zellic \ No newline at end of file diff --git a/wormchain/docs/architecture/0004-tendermint-core-will-be-migrated-to-cometbft-by-bumping-to-latest-cosmos-sdk-upstream-v0-45.md b/wormchain/docs/architecture/0004-tendermint-core-will-be-migrated-to-cometbft-by-bumping-to-latest-cosmos-sdk-upstream-v0-45.md index 7f7f432665..90db868eb6 100644 --- a/wormchain/docs/architecture/0004-tendermint-core-will-be-migrated-to-cometbft-by-bumping-to-latest-cosmos-sdk-upstream-v0-45.md +++ b/wormchain/docs/architecture/0004-tendermint-core-will-be-migrated-to-cometbft-by-bumping-to-latest-cosmos-sdk-upstream-v0-45.md @@ -14,7 +14,7 @@ This version of the Cosmos SDK was released before the fork of Tendermint Core a To facilitate a more modern usage of the Cosmos SDK, projects should move away from Tendermint Core to CometBFT, as it is more up-to-date, maintained and provides security and bug fixes. -The Cosmos SDK team slowly rolled out migrations from Tendermint Core to CometBFT in the Comsos SDK repo, and this migration was implemented in the v0.45 line in release [v0.45.15](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.45.15). +The Cosmos SDK team slowly rolled out migrations from Tendermint Core to CometBFT in the Cosmos SDK repo, and this migration was implemented in the v0.45 line in release [v0.45.15](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.45.15). ## Decision