From ac9ff33b39c187075cf54b0dda7ff51f83d6c6f9 Mon Sep 17 00:00:00 2001 From: Steve <1848680+misko9@users.noreply.github.com> Date: Mon, 13 Nov 2023 12:33:46 -0700 Subject: [PATCH] Add tests for ibc-translator contract (#3301) --- cosmwasm/Cargo.lock | 31 +- cosmwasm/contracts/ibc-translator/Cargo.toml | 8 +- .../contracts/ibc-translator/src/execute.rs | 5 +- .../contracts/ibc-translator/src/reply.rs | 2 +- .../ibc-translator/tests/contract_test.rs | 370 +++++++++ .../ibc-translator/tests/execute_test.rs | 703 ++++++++++++++++++ .../ibc-translator/tests/query_test.rs | 32 + .../ibc-translator/tests/reply_test.rs | 682 +++++++++++++++++ .../ibc-translator/tests/test_setup/mod.rs | 349 +++++++++ 9 files changed, 2177 insertions(+), 5 deletions(-) create mode 100644 cosmwasm/contracts/ibc-translator/tests/contract_test.rs create mode 100644 cosmwasm/contracts/ibc-translator/tests/execute_test.rs create mode 100644 cosmwasm/contracts/ibc-translator/tests/query_test.rs create mode 100644 cosmwasm/contracts/ibc-translator/tests/reply_test.rs create mode 100644 cosmwasm/contracts/ibc-translator/tests/test_setup/mod.rs diff --git a/cosmwasm/Cargo.lock b/cosmwasm/Cargo.lock index 5713ad68ca..0cb4beb2b6 100644 --- a/cosmwasm/Cargo.lock +++ b/cosmwasm/Cargo.lock @@ -498,7 +498,7 @@ dependencies = [ "cw-utils 0.13.4", "derivative", "itertools", - "prost", + "prost 0.9.0", "schemars", "serde", "thiserror", @@ -1023,6 +1023,7 @@ dependencies = [ "anybuf", "anyhow", "bs58", + "cosmwasm-crypto", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.13.4", @@ -1030,6 +1031,9 @@ dependencies = [ "cw20", "cw20-base", "cw20-wrapped-2", + "hex", + "prost 0.11.9", + "serde", "serde-json-wasm 0.5.1", "serde_wormhole", "token-bridge-cosmwasm", @@ -1361,7 +1365,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.9.0", +] + +[[package]] +name = "prost" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +dependencies = [ + "bytes", + "prost-derive 0.11.9", ] [[package]] @@ -1377,6 +1391,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "prost-derive" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "protobuf" version = "2.28.0" diff --git a/cosmwasm/contracts/ibc-translator/Cargo.toml b/cosmwasm/contracts/ibc-translator/Cargo.toml index 71e5e8cc74..800fb37026 100644 --- a/cosmwasm/contracts/ibc-translator/Cargo.toml +++ b/cosmwasm/contracts/ibc-translator/Cargo.toml @@ -29,4 +29,10 @@ serde_wormhole = "0.1.0" token-bridge-cosmwasm = { version = "0.1.0", features = ["library"] } wormhole-bindings = "0.1.0" wormhole-cosmwasm = { version = "0.1.0", features = ["library"] } -wormhole-sdk = { version = "0.1.0", features = ["schemars"] } \ No newline at end of file +wormhole-sdk = { version = "0.1.0", features = ["schemars"] } + +[dev-dependencies] +cosmwasm-crypto = { version = "1.2.7" } +hex = "0.4.3" +prost = "0.11.0" +serde = { version = "1.0.103", features = ["derive", "alloc"]} \ No newline at end of file diff --git a/cosmwasm/contracts/ibc-translator/src/execute.rs b/cosmwasm/contracts/ibc-translator/src/execute.rs index bb45f933a9..a746948af7 100644 --- a/cosmwasm/contracts/ibc-translator/src/execute.rs +++ b/cosmwasm/contracts/ibc-translator/src/execute.rs @@ -114,7 +114,10 @@ pub fn convert_and_transfer( .load(deps.storage) .context("could not load token bridge contract address")?; - ensure!(info.funds.len() == 1, "no bridging coin included"); + ensure!( + info.funds.len() == 1, + "info.funds should contain only 1 coin" + ); let bridging_coin = info.funds[0].clone(); let cw20_contract_addr = parse_bank_token_factory_contract(deps, env, bridging_coin.clone())?; diff --git a/cosmwasm/contracts/ibc-translator/src/reply.rs b/cosmwasm/contracts/ibc-translator/src/reply.rs index 5e3a5568e1..d04287c251 100644 --- a/cosmwasm/contracts/ibc-translator/src/reply.rs +++ b/cosmwasm/contracts/ibc-translator/src/reply.rs @@ -203,7 +203,7 @@ pub fn convert_cw20_to_bank_and_send( } // Base58 allows the subdenom to be a maximum of 44 bytes (max subdenom length) for up to a 32 byte address -fn contract_addr_to_base58(deps: Deps, contract_addr: String) -> Result { +pub fn contract_addr_to_base58(deps: Deps, contract_addr: String) -> Result { // convert the contract address into bytes let contract_addr_bytes = deps.api.addr_canonicalize(&contract_addr).context(format!( "could not canonicalize contract address {contract_addr}" diff --git a/cosmwasm/contracts/ibc-translator/tests/contract_test.rs b/cosmwasm/contracts/ibc-translator/tests/contract_test.rs new file mode 100644 index 0000000000..ef3a2e3fdf --- /dev/null +++ b/cosmwasm/contracts/ibc-translator/tests/contract_test.rs @@ -0,0 +1,370 @@ +use cosmwasm_std::{ + coin, + testing::{mock_dependencies, mock_env, mock_info}, + to_binary, Binary, ContractResult, CosmosMsg, Empty, Event, Reply, ReplyOn, Response, + SubMsgResponse, SystemError, SystemResult, Uint128, WasmMsg, WasmQuery, +}; +use cw_token_bridge::msg::TransferInfoResponse; +use ibc_translator::{ + contract::{execute, instantiate, migrate, query, reply}, + msg::{ChannelResponse, ExecuteMsg, InstantiateMsg, QueryMsg, COMPLETE_TRANSFER_REPLY_ID}, + state::{CHAIN_TO_CHANNEL_MAP, CURRENT_TRANSFER, CW_DENOMS, TOKEN_BRIDGE_CONTRACT}, +}; +use wormhole_bindings::tokenfactory::{TokenFactoryMsg, TokenMsg}; + +mod test_setup; +use test_setup::{ + execute_custom_mock_deps, mock_env_custom_contract, WORMHOLE_CONTRACT_ADDR, WORMHOLE_USER_ADDR, +}; + +// TESTS +// 1. instantiate +// 1. happy path +// 2. migrate +// 1. happy path +// 3. execute +// 1. CompleteTransferAndConvert +// 2. GatewayConvertAndTransfer +// 3. GatewayConvertAndTransferWithPaylod +// 4. SubmitUpdateChainToChannelMap +// 4. reply +// 1. happy path +// 2. no id match +// 5. query +// 1. happy path + +// TESTS: instantiate +// 1. happy path +#[test] +fn instantiate_happy_path() { + let tokenbridge_addr = "faketokenbridge".to_string(); + + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info(WORMHOLE_USER_ADDR, &[]); + let msg = InstantiateMsg { + token_bridge_contract: tokenbridge_addr.clone(), + }; + + let response = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + // response should have 2 attributes + assert_eq!(response.attributes.len(), 2); + assert_eq!(response.attributes[0].key, "action"); + assert_eq!(response.attributes[0].value, "instantiate"); + assert_eq!(response.attributes[1].key, "owner"); + assert_eq!(response.attributes[1].value, WORMHOLE_USER_ADDR); + + // contract addrs should have been set in storage + let saved_tb = TOKEN_BRIDGE_CONTRACT.load(deps.as_mut().storage).unwrap(); + assert_eq!(saved_tb, tokenbridge_addr); +} + +// TESTS: migrate +// 1. happy path +#[test] +fn migrate_happy_path() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = Empty {}; + + let expected_response = Response::::default(); + + let response = migrate(deps.as_mut(), env, msg).unwrap(); + + assert_eq!(response, expected_response); +} + +// TESTS: execute +// 1. CompleteTransferAndConvert +#[test] +fn execute_complete_transfer_and_convert() { + let mut deps = execute_custom_mock_deps(); + let env = mock_env_custom_contract(WORMHOLE_CONTRACT_ADDR); + + let transfer_info_response = TransferInfoResponse { + amount: 1000000u32.into(), + token_address: hex::decode("0000000000000000000000009c3c9283d3e44854697cd22d3faa240cfb032889").unwrap().try_into().unwrap(), + token_chain: 5, + recipient: hex::decode("23aae62840414d69ebc26023d1132f59eef316c82222da4644daaa832ea56349").unwrap().try_into().unwrap(), + recipient_chain: 32, + fee: 0u32.into(), + payload: hex::decode("7b2262617369635f726563697069656e74223a7b22726563697069656e74223a22633256704d575636637a56745a4731334f486436646d4e7a4f585a344f586b335a4774306357646c4d336c36626a52334d477735626a5130227d7d").unwrap(), + }; + let transfer_info_response_copy = transfer_info_response.clone(); + + deps.querier.update_wasm(move |q| match q { + WasmQuery::Smart { + contract_addr: _, + msg: _, + } => SystemResult::Ok(ContractResult::Ok( + to_binary(&transfer_info_response_copy).unwrap(), + )), + _ => SystemResult::Err(SystemError::UnsupportedRequest { + kind: "wasm".to_string(), + }), + }); + + let token_bridge_addr = "faketokenbridge".to_string(); + TOKEN_BRIDGE_CONTRACT + .save(deps.as_mut().storage, &token_bridge_addr) + .unwrap(); + + let info = mock_info(WORMHOLE_USER_ADDR, &[]); + let vaa = Binary::from_base64("AAAAAA").unwrap(); + let msg = ExecuteMsg::CompleteTransferAndConvert { vaa }; + + let response = execute(deps.as_mut(), env, info, msg).unwrap(); + + // response should have 1 message + assert_eq!(response.messages.len(), 1); + + // 1. WasmMsg::Execute (token bridge complete transfer) + assert_eq!(response.messages[0].id, COMPLETE_TRANSFER_REPLY_ID); + assert_eq!(response.messages[0].reply_on, ReplyOn::Success); + assert_eq!( + response.messages[0].msg, + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: token_bridge_addr, + msg: Binary::from_base64("eyJjb21wbGV0ZV90cmFuc2Zlcl93aXRoX3BheWxvYWQiOnsiZGF0YSI6IkFBQUFBQT09IiwicmVsYXllciI6Indvcm1ob2xlMXZoa20ycXY3ODRydWx4OHlscnUwenB2eXZ3M20zY3k5OWU2d3kwIn19").unwrap(), + funds: vec![] + }) + ); + + // response should have 2 attributes + assert_eq!(response.attributes.len(), 2); + assert_eq!(response.attributes[0].key, "action"); + assert_eq!( + response.attributes[0].value, + "complete_transfer_with_payload" + ); + assert_eq!(response.attributes[1].key, "transfer_payload"); + assert_eq!( + response.attributes[1].value, + Binary::from(transfer_info_response.clone().payload).to_base64() + ); + + // finally, validate that the state was saved into storage + let saved_transfer = CURRENT_TRANSFER.load(deps.as_mut().storage).unwrap(); + assert_eq!(saved_transfer, transfer_info_response); +} + +// 2. GatewayConvertAndTransfer +#[test] +fn execute_gateway_convert_and_transfer() { + let mut deps = execute_custom_mock_deps(); + + let token_bridge_addr = "faketokenbridge".to_string(); + TOKEN_BRIDGE_CONTRACT + .save(deps.as_mut().storage, &token_bridge_addr) + .unwrap(); + let tokenfactory_denom = + "factory/cosmos2contract/3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa".to_string(); + CW_DENOMS + .save( + deps.as_mut().storage, + WORMHOLE_CONTRACT_ADDR.to_string(), + &tokenfactory_denom, + ) + .unwrap(); + let coin = coin(1, tokenfactory_denom.clone()); + + let info = mock_info(WORMHOLE_USER_ADDR, &[coin.clone()]); + let env = mock_env(); + let recipient_chain = 2; + let recipient = Binary::from_base64("AAAAAAAAAAAAAAAAjyagAl3Mxs/Aen04dWKAoQ4pWtc=").unwrap(); + let fee = Uint128::zero(); + let nonce = 0u32; + + let msg = ExecuteMsg::GatewayConvertAndTransfer { + recipient, + chain: recipient_chain, + fee, + nonce, + }; + + let response = execute(deps.as_mut(), env, info, msg).unwrap(); + + // response should have 3 messages + assert_eq!(response.messages.len(), 3); + + let mut expected_response: Response = Response::new(); + expected_response = expected_response.add_message(TokenMsg::BurnTokens { + denom: tokenfactory_denom, + amount: coin.amount.u128(), + burn_from_address: "".to_string(), + }); + expected_response = expected_response.add_message( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: WORMHOLE_CONTRACT_ADDR.to_string(), + msg: Binary::from_base64("eyJpbmNyZWFzZV9hbGxvd2FuY2UiOnsic3BlbmRlciI6ImZha2V0b2tlbmJyaWRnZSIsImFtb3VudCI6IjEiLCJleHBpcmVzIjpudWxsfX0=").unwrap(), + funds: vec![] + }) + ); + expected_response = expected_response.add_message( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: token_bridge_addr, + msg: Binary::from_base64("eyJpbml0aWF0ZV90cmFuc2ZlciI6eyJhc3NldCI6eyJpbmZvIjp7InRva2VuIjp7ImNvbnRyYWN0X2FkZHIiOiJ3b3JtaG9sZTF5dzR3djJ6cWc5eGtuNjd6dnEzYXp5ZTB0OGgweDlrZ3lnM2Q1M2p5bTI0Z3h0NDl2ZHlzNnM4aDdhIn19LCJhbW91bnQiOiIxIn0sInJlY2lwaWVudF9jaGFpbiI6MiwicmVjaXBpZW50IjoiQUFBQUFBQUFBQUFBQUFBQWp5YWdBbDNNeHMvQWVuMDRkV0tBb1E0cFd0Yz0iLCJmZWUiOiIwIiwibm9uY2UiOjB9fQ==").unwrap(), + funds: vec![] + }) + ); + + // 1. TokenMsg::BurnTokens + assert_eq!(response.messages[0].msg, expected_response.messages[0].msg,); + + // 2. WasmMsg::Execute (increase allowance) + assert_eq!(response.messages[1].msg, expected_response.messages[1].msg,); + + // 3. WasmMsg::Execute (initiate transfer) + assert_eq!(response.messages[2].msg, expected_response.messages[2].msg,); +} + +// 3. GatewayConvertAndTransferWithPaylod +#[test] +fn execute_gateway_convert_and_transfer_with_payload() { + let mut deps = execute_custom_mock_deps(); + + let token_bridge_addr = "faketokenbridge".to_string(); + TOKEN_BRIDGE_CONTRACT + .save(deps.as_mut().storage, &token_bridge_addr) + .unwrap(); + let tokenfactory_denom = + "factory/cosmos2contract/3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa".to_string(); + CW_DENOMS + .save( + deps.as_mut().storage, + WORMHOLE_CONTRACT_ADDR.to_string(), + &tokenfactory_denom, + ) + .unwrap(); + let coin = coin(1, tokenfactory_denom.clone()); + + let info = mock_info(WORMHOLE_USER_ADDR, &[coin.clone()]); + let env = mock_env(); + let recipient_chain = 2; + let recipient = Binary::from_base64("AAAAAAAAAAAAAAAAjyagAl3Mxs/Aen04dWKAoQ4pWtc=").unwrap(); + let nonce = 0u32; + + let msg = ExecuteMsg::GatewayConvertAndTransferWithPayload { + contract: recipient, + chain: recipient_chain, + payload: Binary::default(), + nonce, + }; + + let response = execute(deps.as_mut(), env, info, msg).unwrap(); + + // response should have 3 messages + assert_eq!(response.messages.len(), 3); + + let mut expected_response: Response = Response::new(); + expected_response = expected_response.add_message(TokenMsg::BurnTokens { + denom: tokenfactory_denom, + amount: coin.amount.u128(), + burn_from_address: "".to_string(), + }); + expected_response = expected_response.add_message( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: WORMHOLE_CONTRACT_ADDR.to_string(), + msg: Binary::from_base64("eyJpbmNyZWFzZV9hbGxvd2FuY2UiOnsic3BlbmRlciI6ImZha2V0b2tlbmJyaWRnZSIsImFtb3VudCI6IjEiLCJleHBpcmVzIjpudWxsfX0=").unwrap(), + funds: vec![] + }) + ); + expected_response = expected_response.add_message( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: token_bridge_addr, + msg: Binary::from_base64("eyJpbml0aWF0ZV90cmFuc2Zlcl93aXRoX3BheWxvYWQiOnsiYXNzZXQiOnsiaW5mbyI6eyJ0b2tlbiI6eyJjb250cmFjdF9hZGRyIjoid29ybWhvbGUxeXc0d3YyenFnOXhrbjY3enZxM2F6eWUwdDhoMHg5a2d5ZzNkNTNqeW0yNGd4dDQ5dmR5czZzOGg3YSJ9fSwiYW1vdW50IjoiMSJ9LCJyZWNpcGllbnRfY2hhaW4iOjIsInJlY2lwaWVudCI6IkFBQUFBQUFBQUFBQUFBQUFqeWFnQWwzTXhzL0FlbjA0ZFdLQW9RNHBXdGM9IiwiZmVlIjoiMCIsInBheWxvYWQiOiIiLCJub25jZSI6MH19").unwrap(), + funds: vec![] + }) + ); + + // 1. TokenMsg::BurnTokens + assert_eq!(response.messages[0].msg, expected_response.messages[0].msg,); + + // 2. WasmMsg::Execute (increase allowance) + assert_eq!(response.messages[1].msg, expected_response.messages[1].msg,); + + // 3. WasmMsg::Execute (initiate transfer) + assert_eq!(response.messages[2].msg, expected_response.messages[2].msg,); +} + +// 4. SubmitUpdateChainToChannelMap +#[test] +fn execute_submit_update_chain_to_channel_map() { + let mut deps = execute_custom_mock_deps(); + let info = mock_info(WORMHOLE_USER_ADDR, &[]); + let env = mock_env(); + let vaa = Binary::from_base64("AQAAAAAFAI84lwdr/G1Uv36wfJpLtlTsfFexBcSjWGOHXt71h43IJNlDRh+FMX4eIpMdyBlY82LEZPGZDT/VetSupFgR4zYBATLRAqUMGfqBraBAMdI12bRk3aV2auwls+juBOuUe+kXOhYrUIQiltr4JGBVQ+VW3Mt7ykM5nOUq/+xWRBdzEuMAAm448B4M67xvIUOw4BaYUz5q5won0hXLR8w0jocO39bXdxksR+ZKTevfEHglmH0ti0lFduMGznqu3AJ8n9WbytcBA3JCC0Jd5PHeu8cAuAnYTsBdeDng1nHzMqUsU9r/2BCsGouEjrqgYicx5StwuBqjyIT7ede2/3wjKfoxOLMMeQUABNR1TWQhY8LEJDgqetXszpsKhh9xeJp3sTPSNpfKxKa8LHL8e4McoHEwbZ3uBMsqNDVVri1vSHxFkrOaLIYIwqsBAAAAAAAAAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAA4gAAAAAAAAAAAAAAAAAAAAAAAAAEliY1RyYW5zbGF0b3IBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY2hhbm5lbC0xAAs=").unwrap(); + + let msg = ExecuteMsg::SubmitUpdateChainToChannelMap { vaa }; + let response = execute(deps.as_mut(), env, info, msg).unwrap(); + + // response should have 0 message + assert_eq!(response.messages.len(), 0); + assert_eq!( + response, + Response::new().add_event( + Event::new("UpdateChainToChannelMap") + .add_attribute("chain_id", "Karura".to_string()) + .add_attribute("channel_id", "channel-1".to_string()), + ) + ); +} + +// TESTS: reply +// 1. Happy path: REPLY ID matches +#[test] +fn reply_happy_path() { + let mut deps = mock_dependencies(); + let env = mock_env(); + + // for this test we don't build a proper reply. + // we're just testing that the handle_complete_transfer_reply method is called when the reply_id is 1 + let msg = Reply { + id: 1, + result: cosmwasm_std::SubMsgResult::Err("random error".to_string()), + }; + + let err = reply(deps.as_mut(), env, msg).unwrap_err(); + assert_eq!( + err.to_string(), + "msg result is not okay, we should never get here" + ); +} + +// 2. ID does not match reply -- no op +#[test] +fn reply_no_id_match() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = Reply { + id: 0, + result: cosmwasm_std::SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: None, + }), + }; + + let err = reply(deps.as_mut(), env, msg).unwrap_err(); + assert_eq!(err.to_string(), "unmatched reply id 0"); +} + +// TEST: query +// 1. happy path +#[test] +fn query_query_ibc_channel_happy_path() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let chain_id: u16 = 0; + let msg = QueryMsg::IbcChannel { chain_id }; + + let channel = "channel-0".to_string(); + CHAIN_TO_CHANNEL_MAP + .save(deps.as_mut().storage, 0, &channel) + .unwrap(); + + let expected_response = to_binary(&ChannelResponse { channel }).unwrap(); + + let response = query(deps.as_ref(), env, msg).unwrap(); + assert_eq!(expected_response, response); +} diff --git a/cosmwasm/contracts/ibc-translator/tests/execute_test.rs b/cosmwasm/contracts/ibc-translator/tests/execute_test.rs new file mode 100644 index 0000000000..ac7ee79e2f --- /dev/null +++ b/cosmwasm/contracts/ibc-translator/tests/execute_test.rs @@ -0,0 +1,703 @@ +use cosmwasm_std::{ + coin, + testing::{mock_env, mock_info, MOCK_CONTRACT_ADDR}, + to_binary, Binary, Coin, ContractResult, CosmosMsg, Event, ReplyOn, Response, SystemError, + SystemResult, Uint128, WasmMsg, WasmQuery, +}; +use cw_token_bridge::msg::TransferInfoResponse; +use ibc_translator::{ + execute::{ + complete_transfer_and_convert, contract_addr_from_base58, convert_and_transfer, + parse_bank_token_factory_contract, submit_update_chain_to_channel_map, TransferType, + }, + msg::COMPLETE_TRANSFER_REPLY_ID, + state::{CURRENT_TRANSFER, CW_DENOMS, TOKEN_BRIDGE_CONTRACT}, +}; +use wormhole_bindings::tokenfactory::{TokenFactoryMsg, TokenMsg}; + +mod test_setup; +use test_setup::{ + execute_custom_mock_deps, mock_env_custom_contract, WORMHOLE_CONTRACT_ADDR, WORMHOLE_USER_ADDR, +}; + +// Tests +// 1. complete_transfer_and_convert +// 1. happy path +// 2. no token bridge state +// 3. failure transferinfo query +// 4. failure humanize recipient +// 5. no match recipient contract +// 2. convert_and_transfer +// 1. happy path +// 2. no token bridge state +// 3. no funds +// 4. too many funds +// 5. parse method failure +// 3. parse_bank_token_factory_contract +// 1. happy path +// 2. failure denom length +// 3. failure non factory token +// 4. failure non contract created +// 5. failure base58 decode failure +// 6. failure no storage +// 7. failure storage mismatch +// 4. contract_addr_from_base58 +// 1. happy path +// 2. failure decode base58 +// 5. submit_update_chain_to_channel_map +// 1. happy path +// 2. failed to parse vaa +// 3. unsupported VAA version +// 4. not a governance vaa +// 5. failed to parse governance packet +// 6. governance vaa is for another chain +// 7. governance vaa already executed +// 8. chain is for wormchain +// 9. failed to parse channel-id + +// TESTS: complete_transfer_and_convert +// 1. Happy path +#[test] +fn complete_transfer_and_convert_happy_path() { + let mut deps = execute_custom_mock_deps(); + let env = mock_env_custom_contract(WORMHOLE_CONTRACT_ADDR); + + let transfer_info_response = TransferInfoResponse { + amount: 1000000u32.into(), + token_address: hex::decode("0000000000000000000000009c3c9283d3e44854697cd22d3faa240cfb032889").unwrap().try_into().unwrap(), + token_chain: 5, + recipient: hex::decode("23aae62840414d69ebc26023d1132f59eef316c82222da4644daaa832ea56349").unwrap().try_into().unwrap(), + recipient_chain: 32, + fee: 0u32.into(), + payload: hex::decode("7b2262617369635f726563697069656e74223a7b22726563697069656e74223a22633256704d575636637a56745a4731334f486436646d4e7a4f585a344f586b335a4774306357646c4d336c36626a52334d477735626a5130227d7d").unwrap(), + }; + let transfer_info_response_copy = transfer_info_response.clone(); + + deps.querier.update_wasm(move |q| match q { + WasmQuery::Smart { + contract_addr: _, + msg: _, + } => SystemResult::Ok(ContractResult::Ok( + to_binary(&transfer_info_response_copy).unwrap(), + )), + _ => SystemResult::Err(SystemError::UnsupportedRequest { + kind: "wasm".to_string(), + }), + }); + + let token_bridge_addr = "faketokenbridge".to_string(); + TOKEN_BRIDGE_CONTRACT + .save(deps.as_mut().storage, &token_bridge_addr) + .unwrap(); + + let info = mock_info(WORMHOLE_USER_ADDR, &[]); + let vaa = Binary::from_base64("AAAAAA").unwrap(); + + let response = complete_transfer_and_convert(deps.as_mut(), env, info, vaa).unwrap(); + + // response should have 1 message + assert_eq!(response.messages.len(), 1); + + // 1. WasmMsg::Execute (token bridge complete transfer) + assert_eq!(response.messages[0].id, COMPLETE_TRANSFER_REPLY_ID); + assert_eq!(response.messages[0].reply_on, ReplyOn::Success); + assert_eq!( + response.messages[0].msg, + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: token_bridge_addr, + msg: Binary::from_base64("eyJjb21wbGV0ZV90cmFuc2Zlcl93aXRoX3BheWxvYWQiOnsiZGF0YSI6IkFBQUFBQT09IiwicmVsYXllciI6Indvcm1ob2xlMXZoa20ycXY3ODRydWx4OHlscnUwenB2eXZ3M20zY3k5OWU2d3kwIn19").unwrap(), + funds: vec![] + }) + ); + + // response should have 2 attributes + assert_eq!(response.attributes.len(), 2); + assert_eq!(response.attributes[0].key, "action"); + assert_eq!( + response.attributes[0].value, + "complete_transfer_with_payload" + ); + assert_eq!(response.attributes[1].key, "transfer_payload"); + assert_eq!( + response.attributes[1].value, + Binary::from(transfer_info_response.clone().payload).to_base64() + ); + + // finally, validate that the state was saved into storage + let saved_transfer = CURRENT_TRANSFER.load(deps.as_mut().storage).unwrap(); + assert_eq!(saved_transfer, transfer_info_response); +} + +// 2. Failure: no token bridge address in state +#[test] +fn complete_transfer_and_convert_no_token_bridge_state() { + let mut deps = execute_custom_mock_deps(); + let info = mock_info(WORMHOLE_USER_ADDR, &[]); + let env = mock_env(); + let vaa = Binary::from_base64("AAAAAA").unwrap(); + + let err = complete_transfer_and_convert(deps.as_mut(), env, info, vaa).unwrap_err(); + assert_eq!( + err.to_string(), + "could not load token bridge contract address" + ); +} + +// 3. Failure: token bridge query TransferInfo failed +#[test] +fn complete_transfer_and_convert_failure_transferinfo_query() { + let mut deps = execute_custom_mock_deps(); + deps.querier.update_wasm(|q| match q { + WasmQuery::Smart { + contract_addr: _, + msg: _, + } => SystemResult::Ok(ContractResult::Err("query failed".to_string())), + _ => SystemResult::Err(SystemError::UnsupportedRequest { + kind: "wasm".to_string(), + }), + }); + + let token_bridge_addr = "faketokenbridge".to_string(); + TOKEN_BRIDGE_CONTRACT + .save(deps.as_mut().storage, &token_bridge_addr) + .unwrap(); + + let info = mock_info(WORMHOLE_USER_ADDR, &[]); + let env = mock_env(); + let vaa = Binary::from_base64("AAAAAA").unwrap(); + + let err = complete_transfer_and_convert(deps.as_mut(), env, info, vaa).unwrap_err(); + assert_eq!(err.to_string(), "could not parse token bridge payload3 vaa"); +} + +// 4. Failure: could not humanize recipient address +#[test] +fn complete_transfer_and_convert_failure_humanize_recipient() { + let mut deps = execute_custom_mock_deps(); + let env = mock_env(); + + let transfer_info_response = to_binary(&cw_token_bridge::msg::TransferInfoResponse { + amount: 1000000u32.into(), + token_address: hex::decode("0000000000000000000000009c3c9283d3e44854697cd22d3faa240cfb032889").unwrap().try_into().unwrap(), + token_chain: 5, + recipient: hex::decode("6d9ae6b2d333c1d65301a59da3eed388ca5dc60cb12496584b75cbe6b15fdbed").unwrap().try_into().unwrap(), + recipient_chain: 32, + fee: 0u32.into(), + payload: hex::decode("7b2262617369635f726563697069656e74223a7b22726563697069656e74223a22633256704d575636637a56745a4731334f486436646d4e7a4f585a344f586b335a4774306357646c4d336c36626a52334d477735626a5130227d7d").unwrap(), + }).unwrap(); + + deps.querier.update_wasm(move |q| match q { + WasmQuery::Smart { + contract_addr: _, + msg: _, + } => SystemResult::Ok(ContractResult::Ok(transfer_info_response.clone())), + _ => SystemResult::Err(SystemError::UnsupportedRequest { + kind: "wasm".to_string(), + }), + }); + + let token_bridge_addr = "faketokenbridge".to_string(); + TOKEN_BRIDGE_CONTRACT + .save(deps.as_mut().storage, &token_bridge_addr) + .unwrap(); + + let info = mock_info(WORMHOLE_USER_ADDR, &[]); + let vaa = Binary::from_base64("AAAAAA").unwrap(); + + let err = complete_transfer_and_convert(deps.as_mut(), env, info, vaa).unwrap_err(); + assert_eq!(err.to_string(), "Generic error: case not found"); +} + +// 5. Failure: recipient address doesn't match contract address +#[test] +fn complete_transfer_and_convert_nomatch_recipient_contract() { + let mut deps = execute_custom_mock_deps(); + let env = mock_env(); + + let transfer_info_response = to_binary(&cw_token_bridge::msg::TransferInfoResponse { + amount: 1000000u32.into(), + token_address: hex::decode("0000000000000000000000009c3c9283d3e44854697cd22d3faa240cfb032889").unwrap().try_into().unwrap(), + token_chain: 5, + recipient: hex::decode("23aae62840414d69ebc26023d1132f59eef316c82222da4644daaa832ea56349").unwrap().try_into().unwrap(), + recipient_chain: 32, + fee: 0u32.into(), + payload: hex::decode("7b2262617369635f726563697069656e74223a7b22726563697069656e74223a22633256704d575636637a56745a4731334f486436646d4e7a4f585a344f586b335a4774306357646c4d336c36626a52334d477735626a5130227d7d").unwrap(), + }).unwrap(); + + deps.querier.update_wasm(move |q| match q { + WasmQuery::Smart { + contract_addr: _, + msg: _, + } => SystemResult::Ok(ContractResult::Ok(transfer_info_response.clone())), + _ => SystemResult::Err(SystemError::UnsupportedRequest { + kind: "wasm".to_string(), + }), + }); + + let token_bridge_addr = "faketokenbridge".to_string(); + TOKEN_BRIDGE_CONTRACT + .save(deps.as_mut().storage, &token_bridge_addr) + .unwrap(); + + let info = mock_info(WORMHOLE_USER_ADDR, &[]); + let vaa = Binary::from_base64("AAAAAA").unwrap(); + + let err = complete_transfer_and_convert(deps.as_mut(), env, info, vaa).unwrap_err(); + assert_eq!(err.to_string(), "vaa recipient must be this contract"); +} + +// TESTS: convert_and_transfer +// 1. Happy path +#[test] +fn convert_and_transfer_happy_path() { + let mut deps = execute_custom_mock_deps(); + + let token_bridge_addr = "faketokenbridge".to_string(); + TOKEN_BRIDGE_CONTRACT + .save(deps.as_mut().storage, &token_bridge_addr) + .unwrap(); + let tokenfactory_denom = + "factory/cosmos2contract/3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa".to_string(); + CW_DENOMS + .save( + deps.as_mut().storage, + WORMHOLE_CONTRACT_ADDR.to_string(), + &tokenfactory_denom, + ) + .unwrap(); + let coin = coin(1, tokenfactory_denom.clone()); + + let info = mock_info(WORMHOLE_USER_ADDR, &[coin.clone()]); + let env = mock_env(); + let recipient_chain = 2; + let recipient = Binary::from_base64("AAAAAAAAAAAAAAAAjyagAl3Mxs/Aen04dWKAoQ4pWtc=").unwrap(); + let fee = Uint128::zero(); + let transfer_type = TransferType::Simple { fee }; + let nonce = 0u32; + + let response = convert_and_transfer( + deps.as_mut(), + info, + env, + recipient, + recipient_chain, + transfer_type, + nonce, + ) + .unwrap(); + + // response should have 3 messages + assert_eq!(response.messages.len(), 3); + + let mut expected_response: Response = Response::new(); + expected_response = expected_response.add_message(TokenMsg::BurnTokens { + denom: tokenfactory_denom, + amount: coin.amount.u128(), + burn_from_address: "".to_string(), + }); + expected_response = expected_response.add_message( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: WORMHOLE_CONTRACT_ADDR.to_string(), + msg: Binary::from_base64("eyJpbmNyZWFzZV9hbGxvd2FuY2UiOnsic3BlbmRlciI6ImZha2V0b2tlbmJyaWRnZSIsImFtb3VudCI6IjEiLCJleHBpcmVzIjpudWxsfX0=").unwrap(), + funds: vec![] + }) + ); + expected_response = expected_response.add_message( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: token_bridge_addr, + msg: Binary::from_base64("eyJpbml0aWF0ZV90cmFuc2ZlciI6eyJhc3NldCI6eyJpbmZvIjp7InRva2VuIjp7ImNvbnRyYWN0X2FkZHIiOiJ3b3JtaG9sZTF5dzR3djJ6cWc5eGtuNjd6dnEzYXp5ZTB0OGgweDlrZ3lnM2Q1M2p5bTI0Z3h0NDl2ZHlzNnM4aDdhIn19LCJhbW91bnQiOiIxIn0sInJlY2lwaWVudF9jaGFpbiI6MiwicmVjaXBpZW50IjoiQUFBQUFBQUFBQUFBQUFBQWp5YWdBbDNNeHMvQWVuMDRkV0tBb1E0cFd0Yz0iLCJmZWUiOiIwIiwibm9uY2UiOjB9fQ==").unwrap(), + funds: vec![] + }) + ); + + // 1. TokenMsg::BurnTokens + assert_eq!(response.messages[0].msg, expected_response.messages[0].msg,); + + // 2. WasmMsg::Execute (increase allowance) + assert_eq!(response.messages[1].msg, expected_response.messages[1].msg,); + + // 3. WasmMsg::Execute (initiate transfer) + assert_eq!(response.messages[2].msg, expected_response.messages[2].msg,); +} + +// 2. Failure: no token bridge address in state +#[test] +fn convert_and_transfer_no_token_bridge_state() { + let mut deps = execute_custom_mock_deps(); + let info = mock_info(WORMHOLE_USER_ADDR, &[]); + let env = mock_env(); + let recipient_chain = 2; + let recipient = Binary::from_base64("AAAAAAAAAAAAAAAAjyagAl3Mxs/Aen04dWKAoQ4pWtc=").unwrap(); + let fee = Uint128::zero(); + let transfer_type = TransferType::Simple { fee }; + let nonce = 0u32; + + let err = convert_and_transfer( + deps.as_mut(), + info, + env, + recipient, + recipient_chain, + transfer_type, + nonce, + ) + .unwrap_err(); + assert_eq!( + err.to_string(), + "could not load token bridge contract address" + ); +} + +// 3. Failure: no coin in funds +#[test] +fn convert_and_transfer_no_funds() { + let mut deps = execute_custom_mock_deps(); + + let token_bridge_addr = "faketokenbridge".to_string(); + TOKEN_BRIDGE_CONTRACT + .save(deps.as_mut().storage, &token_bridge_addr) + .unwrap(); + + let info = mock_info(WORMHOLE_USER_ADDR, &[]); + let env = mock_env(); + let recipient_chain = 2; + let recipient = Binary::from_base64("AAAAAAAAAAAAAAAAjyagAl3Mxs/Aen04dWKAoQ4pWtc=").unwrap(); + let fee = Uint128::zero(); + let transfer_type = TransferType::Simple { fee }; + let nonce = 0u32; + + let err = convert_and_transfer( + deps.as_mut(), + info, + env, + recipient, + recipient_chain, + transfer_type, + nonce, + ) + .unwrap_err(); + assert_eq!(err.to_string(), "info.funds should contain only 1 coin"); +} + +// 4. Failure: more coins than expected in funds +#[test] +fn convert_and_transfer_too_many_funds() { + let mut deps = execute_custom_mock_deps(); + + let token_bridge_addr = "faketokenbridge".to_string(); + TOKEN_BRIDGE_CONTRACT + .save(deps.as_mut().storage, &token_bridge_addr) + .unwrap(); + + let info = mock_info(WORMHOLE_USER_ADDR, &[coin(1, "denomA"), coin(1, "denomB")]); + let env = mock_env(); + let recipient_chain = 2; + let recipient = Binary::from_base64("AAAAAAAAAAAAAAAAjyagAl3Mxs/Aen04dWKAoQ4pWtc=").unwrap(); + let fee = Uint128::zero(); + let transfer_type = TransferType::Simple { fee }; + let nonce = 0u32; + + let err = convert_and_transfer( + deps.as_mut(), + info, + env, + recipient, + recipient_chain, + transfer_type, + nonce, + ) + .unwrap_err(); + assert_eq!(err.to_string(), "info.funds should contain only 1 coin"); +} + +// 5. Failure: parse_bank_token_factory_contract method failure +#[test] +fn convert_and_transfer_parse_method_failure() { + let mut deps = execute_custom_mock_deps(); + + let token_bridge_addr = "faketokenbridge".to_string(); + TOKEN_BRIDGE_CONTRACT + .save(deps.as_mut().storage, &token_bridge_addr) + .unwrap(); + + let info = mock_info(WORMHOLE_USER_ADDR, &[coin(1, "denomA")]); + let env = mock_env(); + let recipient_chain = 2; + let recipient = Binary::from_base64("AAAAAAAAAAAAAAAAjyagAl3Mxs/Aen04dWKAoQ4pWtc=").unwrap(); + let fee = Uint128::zero(); + let transfer_type = TransferType::Simple { fee }; + let nonce = 0u32; + + let err = convert_and_transfer( + deps.as_mut(), + info, + env, + recipient, + recipient_chain, + transfer_type, + nonce, + ) + .unwrap_err(); + assert_eq!(err.to_string(), "coin is not from the token factory"); +} + +// TESTS: parse_bank_token_factory_contract +// 1. Happy path +#[test] +fn parse_bank_token_factory_contract_happy_path() { + let mut deps = execute_custom_mock_deps(); + let env = mock_env(); + + let tokenfactory_denom = format!( + "factory/{}/{}", + MOCK_CONTRACT_ADDR, "3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa" + ); + let coin = Coin::new(100, tokenfactory_denom.clone()); + CW_DENOMS + .save( + deps.as_mut().storage, + WORMHOLE_CONTRACT_ADDR.to_string(), + &tokenfactory_denom, + ) + .unwrap(); + + let contract_addr = parse_bank_token_factory_contract(deps.as_mut(), env, coin).unwrap(); + assert_eq!(contract_addr, WORMHOLE_CONTRACT_ADDR); +} + +// 2. Failure: parsed denom not of length 3 +#[test] +fn parse_bank_token_factory_contract_failure_denom_length() { + let mut deps = execute_custom_mock_deps(); + let env = mock_env(); + let coin = Coin::new(100, "tokenfactory/denom"); + + let method_err = parse_bank_token_factory_contract(deps.as_mut(), env, coin).unwrap_err(); + assert_eq!(method_err.to_string(), "coin is not from the token factory"); +} + +// 3. Failure: parsed denom[0] != "factory" +#[test] +fn parse_bank_token_factory_contract_failure_non_factory_token() { + let mut deps = execute_custom_mock_deps(); + let env = mock_env(); + let coin = Coin::new(100, "tokenfactory/contract/denom"); + + let method_err = parse_bank_token_factory_contract(deps.as_mut(), env, coin).unwrap_err(); + assert_eq!(method_err.to_string(), "coin is not from the token factory"); +} + +// 4. Failure: parsed denom[1] != contract address +#[test] +fn parse_bank_token_factory_contract_failure_non_contract_created() { + let mut deps = execute_custom_mock_deps(); + let env = mock_env(); + let coin = Coin::new(100, "factory/contract/denom"); + + let method_err = parse_bank_token_factory_contract(deps.as_mut(), env, coin).unwrap_err(); + assert_eq!(method_err.to_string(), "coin is not from the token factory"); +} + +// 5. Failure: contract_addr_from_base58 method failure +#[test] +fn parse_bank_token_factory_contract_failure_base58_decode_failure() { + let mut deps = execute_custom_mock_deps(); + let env = mock_env(); + let coin = Coin::new(100, format!("factory/{MOCK_CONTRACT_ADDR}/denom0")); + + let method_err = parse_bank_token_factory_contract(deps.as_mut(), env, coin).unwrap_err(); + assert_eq!( + method_err.to_string(), + "failed to decode base58 subdenom denom0" + ); +} + +// 6. Failure: the parsed contract address is not in CW_DENOMS storage +#[test] +fn parse_bank_token_factory_contract_failure_no_storage() { + let mut deps = execute_custom_mock_deps(); + let env = mock_env(); + let coin = Coin::new( + 100, + format!( + "factory/{}/{}", + MOCK_CONTRACT_ADDR, "3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa" + ), + ); + + let method_err = parse_bank_token_factory_contract(deps.as_mut(), env, coin).unwrap_err(); + assert_eq!( + method_err.to_string(), + "a corresponding denom for the extracted contract addr is not contained in storage" + ); +} + +// 7. Failure: the stored denom doesn't equal the coin's denom +#[test] +fn parse_bank_token_factory_contract_failure_storage_mismatch() { + let mut deps = execute_custom_mock_deps(); + let env = mock_env(); + let coin = Coin::new( + 100, + format!( + "factory/{}/{}", + MOCK_CONTRACT_ADDR, "3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa" + ), + ); + + CW_DENOMS + .save( + deps.as_mut().storage, + WORMHOLE_CONTRACT_ADDR.to_string(), + &"factory/fake/fake".to_string(), + ) + .unwrap(); + + let method_err = parse_bank_token_factory_contract(deps.as_mut(), env, coin).unwrap_err(); + assert_eq!( + method_err.to_string(), + "the stored denom for the contract does not match the actual coin denom" + ); +} + +// TESTS: contract_addr_from_base58 +// 1. Happy path: convert to contract address +#[test] +fn contract_addr_from_base58_happy_path() { + let deps = execute_custom_mock_deps(); + let contract_addr = contract_addr_from_base58( + deps.as_ref(), + "3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa", + ) + .unwrap(); + assert_eq!( + contract_addr, + "wormhole1yw4wv2zqg9xkn67zvq3azye0t8h0x9kgyg3d53jym24gxt49vdys6s8h7a" + ); +} + +// 2. Failure: could not decode base58 +#[test] +fn contract_addr_from_base58_failure_decode_base58() { + let deps = execute_custom_mock_deps(); + let method_err = contract_addr_from_base58( + deps.as_ref(), + "3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETD0", + ) + .unwrap_err(); + assert_eq!( + method_err.to_string(), + "failed to decode base58 subdenom 3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETD0" + ) +} + +// 1. happy path +#[test] +fn submit_update_chain_to_channel_map_happy_path() { + let mut deps = execute_custom_mock_deps(); + let vaa = Binary::from_base64("AQAAAAAFAI84lwdr/G1Uv36wfJpLtlTsfFexBcSjWGOHXt71h43IJNlDRh+FMX4eIpMdyBlY82LEZPGZDT/VetSupFgR4zYBATLRAqUMGfqBraBAMdI12bRk3aV2auwls+juBOuUe+kXOhYrUIQiltr4JGBVQ+VW3Mt7ykM5nOUq/+xWRBdzEuMAAm448B4M67xvIUOw4BaYUz5q5won0hXLR8w0jocO39bXdxksR+ZKTevfEHglmH0ti0lFduMGznqu3AJ8n9WbytcBA3JCC0Jd5PHeu8cAuAnYTsBdeDng1nHzMqUsU9r/2BCsGouEjrqgYicx5StwuBqjyIT7ede2/3wjKfoxOLMMeQUABNR1TWQhY8LEJDgqetXszpsKhh9xeJp3sTPSNpfKxKa8LHL8e4McoHEwbZ3uBMsqNDVVri1vSHxFkrOaLIYIwqsBAAAAAAAAAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAA4gAAAAAAAAAAAAAAAAAAAAAAAAAEliY1RyYW5zbGF0b3IBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY2hhbm5lbC0xAAs=").unwrap(); + + let response = submit_update_chain_to_channel_map(deps.as_mut(), vaa).unwrap(); + + // response should have 0 message + assert_eq!(response.messages.len(), 0); + assert_eq!( + response, + Response::new().add_event( + Event::new("UpdateChainToChannelMap") + .add_attribute("chain_id", "Karura".to_string()) + .add_attribute("channel_id", "channel-1".to_string()), + ) + ); +} + +// 2. failed to parse vaa +#[test] +fn submit_update_chain_to_channel_map_failure_parse_vaa() { + let mut deps = execute_custom_mock_deps(); + let vaa = Binary::from_base64("AAAABQCPOJcHa/xtVL9+sHyaS7ZU7HxXsQXEo1hjh17e9YeNyCTZQ0YfhTF+HiKTHcgZWPNixGTxmQ0/1XrUrqRYEeM2AQEy0QKlDBn6ga2gQDHSNdm0ZN2ldmrsJbPo7gTrlHvpFzoWK1CEIpba+CRgVUPlVtzLe8pDOZzlKv/sVkQXcxLjAAJuOPAeDOu8byFDsOAWmFM+aucKJ9IVy0fMNI6HDt/W13cZLEfmSk3r3xB4JZh9LYtJRXbjBs56rtwCfJ/Vm8rXAQNyQgtCXeTx3rvHALgJ2E7AXXg54NZx8zKlLFPa/9gQrBqLhI66oGInMeUrcLgao8iE+3nXtv98Iyn6MTizDHkFAATUdU1kIWPCxCQ4KnrV7M6bCoYfcXiad7Ez0jaXysSmvCxy/HuDHKBxMG2d7gTLKjQ1Va4tb0h8RZKzmiyGCMKrAQAAAAAAAAABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAOIAAAAAAAAAAAAAAAAAAAAAAAAABJYmNUcmFuc2xhdG9yAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGNoYW5uZWwtMQAL").unwrap(); + + let err = submit_update_chain_to_channel_map(deps.as_mut(), vaa).unwrap_err(); + + assert_eq!(err.to_string(), "failed to parse VAA header") +} + +// 3. unsupported VAA version +#[test] +fn submit_update_chain_to_channel_map_failure_unsupported_vaa_version() { + let mut deps = execute_custom_mock_deps(); + let vaa = Binary::from_base64("AAAAAAAFAI84lwdr/G1Uv36wfJpLtlTsfFexBcSjWGOHXt71h43IJNlDRh+FMX4eIpMdyBlY82LEZPGZDT/VetSupFgR4zYBATLRAqUMGfqBraBAMdI12bRk3aV2auwls+juBOuUe+kXOhYrUIQiltr4JGBVQ+VW3Mt7ykM5nOUq/+xWRBdzEuMAAm448B4M67xvIUOw4BaYUz5q5won0hXLR8w0jocO39bXdxksR+ZKTevfEHglmH0ti0lFduMGznqu3AJ8n9WbytcBA3JCC0Jd5PHeu8cAuAnYTsBdeDng1nHzMqUsU9r/2BCsGouEjrqgYicx5StwuBqjyIT7ede2/3wjKfoxOLMMeQUABNR1TWQhY8LEJDgqetXszpsKhh9xeJp3sTPSNpfKxKa8LHL8e4McoHEwbZ3uBMsqNDVVri1vSHxFkrOaLIYIwqsBAAAAAAAAAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAA4gAAAAAAAAAAAAAAAAAAAAAAAAAEliY1RyYW5zbGF0b3IBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY2hhbm5lbC0xAAs=").unwrap(); + + let err = submit_update_chain_to_channel_map(deps.as_mut(), vaa).unwrap_err(); + + assert_eq!(err.to_string(), "unsupported VAA version") +} + +// 4. not a governance vaa +#[test] +fn submit_update_chain_to_channel_map_failure_not_gov_vaa() { + let mut deps = execute_custom_mock_deps(); + let vaa = Binary::from_base64("AQAAAAAFAI84lwdr/G1Uv36wfJpLtlTsfFexBcSjWGOHXt71h43IJNlDRh+FMX4eIpMdyBlY82LEZPGZDT/VetSupFgR4zYBATLRAqUMGfqBraBAMdI12bRk3aV2auwls+juBOuUe+kXOhYrUIQiltr4JGBVQ+VW3Mt7ykM5nOUq/+xWRBdzEuMAAm448B4M67xvIUOw4BaYUz5q5won0hXLR8w0jocO39bXdxksR+ZKTevfEHglmH0ti0lFduMGznqu3AJ8n9WbytcBA3JCC0Jd5PHeu8cAuAnYTsBdeDng1nHzMqUsU9r/2BCsGouEjrqgYicx5StwuBqjyIT7ede2/3wjKfoxOLMMeQUABNR1TWQhY8LEJDgqetXszpsKhh9xeJp3sTPSNpfKxKa8LHL8e4McoHEwbZ3uBMsqNDVVri1vSHxFkrOaLIYIwqsBAAAAAAAAAAEAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAA4gAAAAAAAAAAAAAAAAAAAAAAAAAEliY1RyYW5zbGF0b3IBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY2hhbm5lbC0xAAs=").unwrap(); + + let err = submit_update_chain_to_channel_map(deps.as_mut(), vaa).unwrap_err(); + + assert_eq!(err.to_string(), "not a governance VAA") +} + +// 5. failed to parse governance packet +#[test] +fn submit_update_chain_to_channel_map_failed_parsing_gov_packet() { + let mut deps = execute_custom_mock_deps(); + let vaa = Binary::from_base64("AQAAAAAFAI84lwdr/G1Uv36wfJpLtlTsfFexBcSjWGOHXt71h43IJNlDRh+FMX4eIpMdyBlY82LEZPGZDT/VetSupFgR4zYBATLRAqUMGfqBraBAMdI12bRk3aV2auwls+juBOuUe+kXOhYrUIQiltr4JGBVQ+VW3Mt7ykM5nOUq/+xWRBdzEuMAAm448B4M67xvIUOw4BaYUz5q5won0hXLR8w0jocO39bXdxksR+ZKTevfEHglmH0ti0lFduMGznqu3AJ8n9WbytcBA3JCC0Jd5PHeu8cAuAnYTsBdeDng1nHzMqUsU9r/2BCsGouEjrqgYicx5StwuBqjyIT7ede2/3wjKfoxOLMMeQUABNR1TWQhY8LEJDgqetXszpsKhh9xeJp3sTPSNpfKxKa8LHL8e4McoHEwbZ3uBMsqNDVVri1vSHxFkrOaLIYIwqsBAAAAAAAAAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAA4gAAAAAAAAAAAAAAAAAAAAAAAAAEliY1RyYW5zbGF0b3IBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY2hhbm5lbC0xAAsR").unwrap(); + + let err = submit_update_chain_to_channel_map(deps.as_mut(), vaa).unwrap_err(); + + assert_eq!(err.to_string(), "failed to parse governance packet") +} + +// 6. governance vaa is for another chain +#[test] +fn submit_update_chain_to_channel_map_failure_gov_vaa_for_another_chain() { + let mut deps = execute_custom_mock_deps(); + let vaa = Binary::from_base64("AQAAAAAFAI84lwdr/G1Uv36wfJpLtlTsfFexBcSjWGOHXt71h43IJNlDRh+FMX4eIpMdyBlY82LEZPGZDT/VetSupFgR4zYBATLRAqUMGfqBraBAMdI12bRk3aV2auwls+juBOuUe+kXOhYrUIQiltr4JGBVQ+VW3Mt7ykM5nOUq/+xWRBdzEuMAAm448B4M67xvIUOw4BaYUz5q5won0hXLR8w0jocO39bXdxksR+ZKTevfEHglmH0ti0lFduMGznqu3AJ8n9WbytcBA3JCC0Jd5PHeu8cAuAnYTsBdeDng1nHzMqUsU9r/2BCsGouEjrqgYicx5StwuBqjyIT7ede2/3wjKfoxOLMMeQUABNR1TWQhY8LEJDgqetXszpsKhh9xeJp3sTPSNpfKxKa8LHL8e4McoHEwbZ3uBMsqNDVVri1vSHxFkrOaLIYIwqsBAAAAAAAAAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAA4gAAAAAAAAAAAAAAAAAAAAAAAAAEliY1RyYW5zbGF0b3IBAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY2hhbm5lbC0xAAs=").unwrap(); + + let err = submit_update_chain_to_channel_map(deps.as_mut(), vaa).unwrap_err(); + + assert_eq!(err.to_string(), "this governance VAA is for another chain") +} + +// 7. governance vaa already executed +#[test] +fn submit_update_chain_to_channel_map_failure_gov_vaa_already_executed() { + let mut deps = execute_custom_mock_deps(); + let vaa = Binary::from_base64("AQAAAAAFAI84lwdr/G1Uv36wfJpLtlTsfFexBcSjWGOHXt71h43IJNlDRh+FMX4eIpMdyBlY82LEZPGZDT/VetSupFgR4zYBATLRAqUMGfqBraBAMdI12bRk3aV2auwls+juBOuUe+kXOhYrUIQiltr4JGBVQ+VW3Mt7ykM5nOUq/+xWRBdzEuMAAm448B4M67xvIUOw4BaYUz5q5won0hXLR8w0jocO39bXdxksR+ZKTevfEHglmH0ti0lFduMGznqu3AJ8n9WbytcBA3JCC0Jd5PHeu8cAuAnYTsBdeDng1nHzMqUsU9r/2BCsGouEjrqgYicx5StwuBqjyIT7ede2/3wjKfoxOLMMeQUABNR1TWQhY8LEJDgqetXszpsKhh9xeJp3sTPSNpfKxKa8LHL8e4McoHEwbZ3uBMsqNDVVri1vSHxFkrOaLIYIwqsBAAAAAAAAAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAA4gAAAAAAAAAAAAAAAAAAAAAAAAAEliY1RyYW5zbGF0b3IBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY2hhbm5lbC0xAAs=").unwrap(); + + submit_update_chain_to_channel_map(deps.as_mut(), vaa.clone()).unwrap(); + let err = submit_update_chain_to_channel_map(deps.as_mut(), vaa).unwrap_err(); + + assert_eq!(err.to_string(), "governance vaa already executed") +} + +// 8. chain is for wormchain +#[test] +fn submit_update_chain_to_channel_map_failure_chain_id_is_wormchain() { + let mut deps = execute_custom_mock_deps(); + let vaa = Binary::from_base64("AQAAAAAFAI84lwdr/G1Uv36wfJpLtlTsfFexBcSjWGOHXt71h43IJNlDRh+FMX4eIpMdyBlY82LEZPGZDT/VetSupFgR4zYBATLRAqUMGfqBraBAMdI12bRk3aV2auwls+juBOuUe+kXOhYrUIQiltr4JGBVQ+VW3Mt7ykM5nOUq/+xWRBdzEuMAAm448B4M67xvIUOw4BaYUz5q5won0hXLR8w0jocO39bXdxksR+ZKTevfEHglmH0ti0lFduMGznqu3AJ8n9WbytcBA3JCC0Jd5PHeu8cAuAnYTsBdeDng1nHzMqUsU9r/2BCsGouEjrqgYicx5StwuBqjyIT7ede2/3wjKfoxOLMMeQUABNR1TWQhY8LEJDgqetXszpsKhh9xeJp3sTPSNpfKxKa8LHL8e4McoHEwbZ3uBMsqNDVVri1vSHxFkrOaLIYIwqsBAAAAAAAAAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAA4gAAAAAAAAAAAAAAAAAAAAAAAAAEliY1RyYW5zbGF0b3IBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY2hhbm5lbC0xDCA=").unwrap(); + + let err = submit_update_chain_to_channel_map(deps.as_mut(), vaa).unwrap_err(); + + assert_eq!( + err.to_string(), + "the ibc-translator contract should not maintain channel mappings to wormchain" + ) +} +// 9. failed to parse channel-id +#[test] +fn submit_update_chain_to_channel_map_invalid_channel_id() { + let mut deps = execute_custom_mock_deps(); + let vaa = Binary::from_base64("AQAAAAAFAI84lwdr/G1Uv36wfJpLtlTsfFexBcSjWGOHXt71h43IJNlDRh+FMX4eIpMdyBlY82LEZPGZDT/VetSupFgR4zYBATLRAqUMGfqBraBAMdI12bRk3aV2auwls+juBOuUe+kXOhYrUIQiltr4JGBVQ+VW3Mt7ykM5nOUq/+xWRBdzEuMAAm448B4M67xvIUOw4BaYUz5q5won0hXLR8w0jocO39bXdxksR+ZKTevfEHglmH0ti0lFduMGznqu3AJ8n9WbytcBA3JCC0Jd5PHeu8cAuAnYTsBdeDng1nHzMqUsU9r/2BCsGouEjrqgYicx5StwuBqjyIT7ede2/3wjKfoxOLMMeQUABNR1TWQhY8LEJDgqetXszpsKhh9xeJp3sTPSNpfKxKa8LHL8e4McoHEwbZ3uBMsqNDVVri1vSHxFkrOaLIYIwqsBAAAAAAAAAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAA4gAAAAAAAAAAAAAAAAAAAAAAAAAEliY1RyYW5zbGF0b3IBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY2hhbm5lAC3/AAs=").unwrap(); + + let err = submit_update_chain_to_channel_map(deps.as_mut(), vaa).unwrap_err(); + + assert_eq!(err.to_string(), "failed to parse channel-id as utf-8") +} diff --git a/cosmwasm/contracts/ibc-translator/tests/query_test.rs b/cosmwasm/contracts/ibc-translator/tests/query_test.rs new file mode 100644 index 0000000000..55c006ef9f --- /dev/null +++ b/cosmwasm/contracts/ibc-translator/tests/query_test.rs @@ -0,0 +1,32 @@ +use ibc_translator::{msg::ChannelResponse, query::query_ibc_channel, state::CHAIN_TO_CHANNEL_MAP}; + +use cosmwasm_std::testing::mock_dependencies; + +// Tests +// 1. query_ibc_channel +// 1. happy path +// 2. No chain id to channel mapping + +// 1. happy path +#[test] +fn query_ibc_channel_happy_path() { + let mut deps = mock_dependencies(); + + let channel = "channel-0".to_string(); + CHAIN_TO_CHANNEL_MAP + .save(deps.as_mut().storage, 0, &channel) + .unwrap(); + + let expected_response = ChannelResponse { channel }; + + let response = query_ibc_channel(deps.as_ref(), 0).unwrap(); + assert_eq!(expected_response, response); +} +// 2. No chain id to channel mapping +#[test] +fn query_ibc_channel_no_chain_id() { + let deps = mock_dependencies(); + + let err = query_ibc_channel(deps.as_ref(), 0).unwrap_err(); + assert_eq!(err.to_string(), "alloc::string::String not found"); +} diff --git a/cosmwasm/contracts/ibc-translator/tests/reply_test.rs b/cosmwasm/contracts/ibc-translator/tests/reply_test.rs new file mode 100644 index 0000000000..be0c53c191 --- /dev/null +++ b/cosmwasm/contracts/ibc-translator/tests/reply_test.rs @@ -0,0 +1,682 @@ +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env}, + to_binary, to_vec, Binary, ContractResult, + CosmosMsg::Stargate, + Reply, Response, SubMsgResponse, SystemError, SystemResult, Uint128, WasmQuery, +}; +use cw20::TokenInfoResponse; +use cw_token_bridge::msg::{AssetInfo, CompleteTransferResponse, TransferInfoResponse}; +use ibc_translator::{ + reply::{ + contract_addr_to_base58, convert_cw20_to_bank_and_send, handle_complete_transfer_reply, + }, + state::{CHAIN_TO_CHANNEL_MAP, CURRENT_TRANSFER, CW_DENOMS}, +}; +use prost::Message; +use wormhole_bindings::tokenfactory::{DenomUnit, Metadata, TokenFactoryMsg, TokenMsg}; +use wormhole_sdk::Chain; + +mod test_setup; +use test_setup::{default_custom_mock_deps, WORMHOLE_CONTRACT_ADDR, WORMHOLE_USER_ADDR}; + +#[derive(Clone, PartialEq, Message)] +struct MsgExecuteContractResponse { + #[prost(bytes, tag = "1")] + pub data: ::prost::alloc::vec::Vec, +} + +// Tests +// 1. handle_complete_transfer_reply +// 1. happy path, GatewayTransfer +// 2. happy path, GatewayTransferWithPayload +// 2. bad msg result +// 3. invalid response data +// 4. no repsonse data +// 5. invalid reponse data type +// 6. no reponse contract +// 7. no storage +// 8. wrong stored payload +// 9. invalid recipient +// 10. invalid contract +// 2. convert_cw20_to_bank_and_send +// 1. happy path +// 2. happy path create denom +// 3. failure invalid contract +// 4. chain id no channel +// 5. bad payload +// 3. contract_addr_to_base58 +// 1. happy path +// 2. bad contract address + +// TESTS: handle_complete_transfer_reply +// 1. Happy path: GatewayTransfer +#[test] +fn handle_complete_transfer_reply_happy_path() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = Reply { + id: 1, + result: cosmwasm_std::SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(Binary::from_base64("Cv0BeyJjb250cmFjdCI6Indvcm1ob2xlMXl3NHd2MnpxZzl4a242N3p2cTNhenllMHQ4aDB4OWtneWczZDUzanltMjRneHQ0OXZkeXM2czhoN2EiLCJkZW5vbSI6bnVsbCwicmVjaXBpZW50Ijoic2VpMWRrZHdkdmtueDBxYXY1Y3A1a3c2OG1rbjNyOTltM3N2a3lqZnZrenR3aDk3ZHYybG0wa3NqNnhyYWsiLCJhbW91bnQiOiIxMDAwIiwicmVsYXllciI6InNlaTF2aGttMnF2Nzg0cnVseDh5bHJ1MHpwdnl2dzNtM2N5OXgzeHlmdiIsImZlZSI6IjAifQ==").unwrap()) + }) + }; + + let contract_addr = + "wormhole1yw4wv2zqg9xkn67zvq3azye0t8h0x9kgyg3d53jym24gxt49vdys6s8h7a".to_string(); + let tokenfactory_denom = + "factory/cosmos2contract/3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa".to_string(); + CW_DENOMS + .save(deps.as_mut().storage, contract_addr, &tokenfactory_denom) + .unwrap(); + + let channel = "channel-0".to_string(); + CHAIN_TO_CHANNEL_MAP + .save(deps.as_mut().storage, 0, &channel) + .unwrap(); + + let transfer_payload = TransferInfoResponse { + amount: 0u32.into(), + token_address: [0; 32], + token_chain: 0, + recipient: [0; 32], + recipient_chain: 0, + fee: 0u32.into(), + payload: hex::decode("7B22676174657761795F7472616E73666572223A7B22636861696E223A302C22726563697069656E74223A22633256704D575636637A56745A4731334F486436646D4E7A4F585A344F586B335A4774306357646C4D336C36626A52334D477735626A5130222C22666565223A2230222C226E6F6E6365223A307D7D").unwrap() + }; + CURRENT_TRANSFER + .save(deps.as_mut().storage, &transfer_payload) + .unwrap(); + + // just verifying that we called the convert_cw20_to_bank -- unwrap the result without an error + // other tests verify the correctness of this method + // wormhole core and token bridge tests verify the correctness of the VAA parameters + handle_complete_transfer_reply(deps.as_mut(), env, msg).unwrap(); +} + +// 2. Happy path: GatewayTransferWithPayload +#[test] +fn handle_complete_transfer_reply_happy_path_with_payload() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = Reply { + id: 1, + result: cosmwasm_std::SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(Binary::from_base64("Cv0BeyJjb250cmFjdCI6Indvcm1ob2xlMXl3NHd2MnpxZzl4a242N3p2cTNhenllMHQ4aDB4OWtneWczZDUzanltMjRneHQ0OXZkeXM2czhoN2EiLCJkZW5vbSI6bnVsbCwicmVjaXBpZW50Ijoic2VpMWRrZHdkdmtueDBxYXY1Y3A1a3c2OG1rbjNyOTltM3N2a3lqZnZrenR3aDk3ZHYybG0wa3NqNnhyYWsiLCJhbW91bnQiOiIxMDAwIiwicmVsYXllciI6InNlaTF2aGttMnF2Nzg0cnVseDh5bHJ1MHpwdnl2dzNtM2N5OXgzeHlmdiIsImZlZSI6IjAifQ==").unwrap()) + }) + }; + + let contract_addr = + "wormhole1yw4wv2zqg9xkn67zvq3azye0t8h0x9kgyg3d53jym24gxt49vdys6s8h7a".to_string(); + let tokenfactory_denom = + "factory/cosmos2contract/3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa".to_string(); + CW_DENOMS + .save(deps.as_mut().storage, contract_addr, &tokenfactory_denom) + .unwrap(); + + let channel = "channel-0".to_string(); + CHAIN_TO_CHANNEL_MAP + .save(deps.as_mut().storage, 0, &channel) + .unwrap(); + + let transfer_payload = TransferInfoResponse { + amount: 0u32.into(), + token_address: [0; 32], + token_chain: 0, + recipient: [0; 32], + recipient_chain: 0, + fee: 0u32.into(), + payload: hex::decode("7B22676174657761795F7472616E736665725F776974685F7061796C6F6164223A7B22636861696E223A302C22636F6E7472616374223A22633256704D575636637A56745A4731334F486436646D4E7A4F585A344F586B335A4774306357646C4D336C36626A52334D477735626A5130222C227061796C6F6164223A225647567A64464268655778765957513D222C226E6F6E6365223A307D7D").unwrap() + }; + CURRENT_TRANSFER + .save(deps.as_mut().storage, &transfer_payload) + .unwrap(); + + // just verifying that we called the convert_cw20_to_bank -- unwrap the result without an error + // other tests verify the correctness of this method + // wormhole core and token bridge tests verify the correctness of the VAA parameters + handle_complete_transfer_reply(deps.as_mut(), env, msg).unwrap(); +} + +// 3. Failure: msg result is not okay +#[test] +fn handle_complete_transfer_reply_bad_msg_result() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = Reply { + id: 1, + result: cosmwasm_std::SubMsgResult::Err("random error".to_string()), + }; + + let err = handle_complete_transfer_reply(deps.as_mut(), env, msg).unwrap_err(); + assert_eq!( + err.to_string(), + "msg result is not okay, we should never get here" + ); +} + +// 4. Failure: could not parse reply response_data +#[test] +fn handle_complete_transfer_reply_invalid_response_data() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = Reply { + id: 1, + result: cosmwasm_std::SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(Binary::from_base64("eyJiYXNpY19yZWNpcGllbnQiOnsicmVjaXBpZW50IjoiYzJWcE1XVjZjelZ0WkcxM09IZDZkbU56T1haNE9YazNaR3QwY1dkbE0zbDZialIzTUd3NWJqUTAifX0=").unwrap()) + }) + }; + + let err = handle_complete_transfer_reply(deps.as_mut(), env, msg).unwrap_err(); + assert_eq!( + err.to_string(), + "failed to parse protobuf reply response_data" + ); +} + +// 5. Failure: no data in the parsed response +#[test] +fn handle_complete_transfer_reply_no_response_data() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = Reply { + id: 1, + result: cosmwasm_std::SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(Binary::from_base64("").unwrap()), + }), + }; + + let err = handle_complete_transfer_reply(deps.as_mut(), env, msg).unwrap_err(); + assert_eq!( + err.to_string(), + "no data in the response, we should never get here" + ); +} + +// 6. Failure: could not deserialize response data +#[test] +fn handle_complete_transfer_reply_invalid_response_data_type() { + let mut deps = mock_dependencies(); + let env = mock_env(); + + // encode a payload as protobuf + // the payload is NOT a CompleteTransferResponse + let execute_reply = MsgExecuteContractResponse { + data: to_vec(&AssetInfo::NativeToken { + denom: "denomA".to_string(), + }) + .unwrap(), + }; + let mut encoded_execute_reply = Vec::::with_capacity(execute_reply.encoded_len()); + execute_reply.encode(&mut encoded_execute_reply).unwrap(); + + let msg = Reply { + id: 1, + result: cosmwasm_std::SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(encoded_execute_reply.into()), + }), + }; + + let err = handle_complete_transfer_reply(deps.as_mut(), env, msg).unwrap_err(); + assert_eq!(err.to_string(), "failed to deserialize response data"); +} + +// 7. Failure: no contract in the response +#[test] +fn handle_complete_transfer_reply_no_response_contract() { + let mut deps = mock_dependencies(); + let env = mock_env(); + + // encode a payload as protobuf + // the payload is a CompleteTransferResponse + let execute_reply = MsgExecuteContractResponse { + data: to_vec(&CompleteTransferResponse { + contract: None, + denom: None, + recipient: "fake".to_string(), + amount: 1u32.into(), + relayer: "fake".to_string(), + fee: 0u32.into(), + }) + .unwrap(), + }; + let mut encoded_execute_reply = Vec::::with_capacity(execute_reply.encoded_len()); + execute_reply.encode(&mut encoded_execute_reply).unwrap(); + + let msg = Reply { + id: 1, + result: cosmwasm_std::SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(encoded_execute_reply.into()), + }), + }; + + let err = handle_complete_transfer_reply(deps.as_mut(), env, msg).unwrap_err(); + assert_eq!( + err.to_string(), + "no contract in response, we should never get here" + ); +} + +// 8. Failure: no current transfer in storage +#[test] +fn handle_complete_transfer_reply_no_storage() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = Reply { + id: 1, + result: cosmwasm_std::SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(Binary::from_base64("CvgBeyJjb250cmFjdCI6InNlaTE0MG02eGFnbXcwemVzZWp6aHN2azQ2enByZ3Njcjd0dTk0aDM2cndzdXRjc3hjczRmbWRzOXNldnltIiwiZGVub20iOm51bGwsInJlY2lwaWVudCI6InNlaTFka2R3ZHZrbngwcWF2NWNwNWt3Njhta24zcjk5bTNzdmt5amZ2a3p0d2g5N2R2MmxtMGtzajZ4cmFrIiwiYW1vdW50IjoiMTAwMCIsInJlbGF5ZXIiOiJzZWkxdmhrbTJxdjc4NHJ1bHg4eWxydTB6cHZ5dnczbTNjeTl4M3h5ZnYiLCJmZWUiOiIwIn0=").unwrap()) + }) + }; + + let err = handle_complete_transfer_reply(deps.as_mut(), env, msg).unwrap_err(); + assert_eq!( + err.to_string(), + "failed to load current transfer from storage" + ); +} + +// 9. Failure: could not deserialize payload3 payload from stored transfer +#[test] +fn handle_complete_transfer_reply_wrong_stored_payload() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = Reply { + id: 1, + result: cosmwasm_std::SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(Binary::from_base64("CvgBeyJjb250cmFjdCI6InNlaTE0MG02eGFnbXcwemVzZWp6aHN2azQ2enByZ3Njcjd0dTk0aDM2cndzdXRjc3hjczRmbWRzOXNldnltIiwiZGVub20iOm51bGwsInJlY2lwaWVudCI6InNlaTFka2R3ZHZrbngwcWF2NWNwNWt3Njhta24zcjk5bTNzdmt5amZ2a3p0d2g5N2R2MmxtMGtzajZ4cmFrIiwiYW1vdW50IjoiMTAwMCIsInJlbGF5ZXIiOiJzZWkxdmhrbTJxdjc4NHJ1bHg4eWxydTB6cHZ5dnczbTNjeTl4M3h5ZnYiLCJmZWUiOiIwIn0=").unwrap()) + }) + }; + + let bad_transfer_payload = TransferInfoResponse { + amount: 0u32.into(), + token_address: [0; 32], + token_chain: 0, + recipient: [0; 32], + recipient_chain: 0, + fee: 0u32.into(), + payload: hex::decode("7b22726563697069656e74223a7b22726563697069656e74223a22633256704d575636637a56745a4731334f486436646d4e7a4f585a344f586b335a4774306357646c4d336c36626a52334d477735626a5130227d7d").unwrap() + }; + CURRENT_TRANSFER + .save(deps.as_mut().storage, &bad_transfer_payload) + .unwrap(); + + let err = handle_complete_transfer_reply(deps.as_mut(), env, msg).unwrap_err(); + assert_eq!(err.to_string(), "failed to deserialize transfer payload"); +} + +// 10. Failure: could not convert the recipient (GatewayTransfer) base64 encoded bytes to a utf8 string +#[test] +fn handle_complete_transfer_reply_invalid_recipient() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = Reply { + id: 1, + result: cosmwasm_std::SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(Binary::from_base64("CvgBeyJjb250cmFjdCI6InNlaTE0MG02eGFnbXcwemVzZWp6aHN2azQ2enByZ3Njcjd0dTk0aDM2cndzdXRjc3hjczRmbWRzOXNldnltIiwiZGVub20iOm51bGwsInJlY2lwaWVudCI6InNlaTFka2R3ZHZrbngwcWF2NWNwNWt3Njhta24zcjk5bTNzdmt5amZ2a3p0d2g5N2R2MmxtMGtzajZ4cmFrIiwiYW1vdW50IjoiMTAwMCIsInJlbGF5ZXIiOiJzZWkxdmhrbTJxdjc4NHJ1bHg4eWxydTB6cHZ5dnczbTNjeTl4M3h5ZnYiLCJmZWUiOiIwIn0=").unwrap()) + }) + }; + + let bad_transfer_payload = TransferInfoResponse { + amount: 0u32.into(), + token_address: [0; 32], + token_chain: 0, + recipient: [0; 32], + recipient_chain: 0, + fee: 0u32.into(), + payload: hex::decode("7B22676174657761795F7472616E73666572223A7B22636861696E223A302C22726563697069656E74223A223256704D575636637A56745A4731334F486436646D4E7A4F585A344F586B335A4774306357646C4D336C36626A52334D477735626A5130222C22666565223A2230222C226E6F6E6365223A307D7D").unwrap() + }; + CURRENT_TRANSFER + .save(deps.as_mut().storage, &bad_transfer_payload) + .unwrap(); + + let err = handle_complete_transfer_reply(deps.as_mut(), env, msg).unwrap_err(); + assert_eq!( + err.to_string(), + "failed to convert 2VpMWV6czVtZG13OHd6dmNzOXZ4OXk3ZGt0cWdlM3l6bjR3MGw5bjQ0= to utf8 string" + ); +} + +// 11. Failure: could not convert the contract (GatewayTransferWithPayload) base64 encoded bytes to a utf8 string +#[test] +fn handle_complete_transfer_reply_invalid_contract() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = Reply { + id: 1, + result: cosmwasm_std::SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(Binary::from_base64("Cv0BeyJjb250cmFjdCI6Indvcm1ob2xlMXl3NHd2MnpxZzl4a242N3p2cTNhenllMHQ4aDB4OWtneWczZDUzanltMjRneHQ0OXZkeXM2czhoN2EiLCJkZW5vbSI6bnVsbCwicmVjaXBpZW50Ijoic2VpMWRrZHdkdmtueDBxYXY1Y3A1a3c2OG1rbjNyOTltM3N2a3lqZnZrenR3aDk3ZHYybG0wa3NqNnhyYWsiLCJhbW91bnQiOiIxMDAwIiwicmVsYXllciI6InNlaTF2aGttMnF2Nzg0cnVseDh5bHJ1MHpwdnl2dzNtM2N5OXgzeHlmdiIsImZlZSI6IjAifQ==").unwrap()) + }) + }; + + let bad_transfer_payload = TransferInfoResponse { + amount: 0u32.into(), + token_address: [0; 32], + token_chain: 0, + recipient: [0; 32], + recipient_chain: 0, + fee: 0u32.into(), + payload: hex::decode("7B22676174657761795F7472616E736665725F776974685F7061796C6F6164223A7B22636861696E223A302C22636F6E7472616374223A223256704D575636637A56745A4731334F486436646D4E7A4F585A344F586B335A4774306357646C4D336C36626A52334D477735626A5130222C227061796C6F6164223A225647567A64464268655778765957513D222C226E6F6E6365223A307D7D").unwrap() + }; + CURRENT_TRANSFER + .save(deps.as_mut().storage, &bad_transfer_payload) + .unwrap(); + + // just verifying that we called the convert_cw20_to_bank -- unwrap the result without an error + // other tests verify the correctness of this method + // wormhole core and token bridge tests verify the correctness of the VAA parameters + let err = handle_complete_transfer_reply(deps.as_mut(), env, msg).unwrap_err(); + assert_eq!( + err.to_string(), + "failed to convert 2VpMWV6czVtZG13OHd6dmNzOXZ4OXk3ZGt0cWdlM3l6bjR3MGw5bjQ0= to utf8 string" + ); +} + +// Test convert_cw20_to_bank_and_send +// TESTS: convert_cw20_to_bank +// 1. Happy path +#[test] +fn convert_cw20_to_bank_and_send_happy_path() { + let mut deps = default_custom_mock_deps(); + let env = mock_env(); + let recipient = WORMHOLE_USER_ADDR.to_string(); + let amount = 1; + let contract_addr = WORMHOLE_CONTRACT_ADDR.to_string(); + + let tokenfactory_denom = + "factory/cosmos2contract/3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa".to_string(); + CW_DENOMS + .save( + deps.as_mut().storage, + contract_addr.clone(), + &tokenfactory_denom, + ) + .unwrap(); + + let chain_id = Chain::Ethereum; + let channel = "channel-0".to_string(); + CHAIN_TO_CHANNEL_MAP + .save(deps.as_mut().storage, chain_id.into(), &channel) + .unwrap(); + + let response = convert_cw20_to_bank_and_send( + deps.as_mut(), + env, + recipient, + amount, + contract_addr, + chain_id.into(), + None, + ) + .unwrap(); + + // response should have 2 messages: + assert_eq!(response.messages.len(), 2); + + let mut expected_response: Response = Response::new(); + expected_response = expected_response.add_message(TokenMsg::MintTokens { + denom: tokenfactory_denom, + amount, + mint_to_address: "cosmos2contract".to_string(), + }); + expected_response = expected_response.add_message(Stargate { + type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), + value: Binary::from_base64("Cgh0cmFuc2ZlchIJY2hhbm5lbC0wGkkKRGZhY3RvcnkvY29zbW9zMmNvbnRyYWN0LzNRRVF5aTdpeUpId1E0d2ZVTUxGUEI0a1J6Y3pNQVhDaXRXaDdoNlRFVERhEgExIg9jb3Ntb3MyY29udHJhY3QqL3dvcm1ob2xlMXZoa20ycXY3ODRydWx4OHlscnUwenB2eXZ3M20zY3k5OWU2d3kwOL2i6MjOsZzqFQ==").unwrap(), + }); + + // 1. TokenMsg::MintTokens + assert_eq!(response.messages[0].msg, expected_response.messages[0].msg,); + + // 2. Stargate ibc transfer + assert_eq!(response.messages[1].msg, expected_response.messages[1].msg,); +} + +// 2. Happy path + CreateDenom on TokenFactory +#[test] +fn convert_cw20_to_bank_happy_path_create_denom() { + let mut deps = default_custom_mock_deps(); + let env = mock_env(); + let recipient = WORMHOLE_USER_ADDR.to_string(); + let amount = 1; + let contract_addr = WORMHOLE_CONTRACT_ADDR.to_string(); + + let tokenfactory_denom = + "factory/cosmos2contract/3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa".to_string(); + let subdenom = "3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa".to_string(); + + let chain_id = Chain::Ethereum; + let channel = "channel-0".to_string(); + CHAIN_TO_CHANNEL_MAP + .save(deps.as_mut().storage, chain_id.into(), &channel) + .unwrap(); + + let token_info_response = TokenInfoResponse { + name: "TestCoin".to_string(), + symbol: "TEST".to_string(), + decimals: 6, + total_supply: Uint128::new(10_000_000), + }; + let token_info_response_copy = token_info_response.clone(); + deps.querier.update_wasm(move |q| match q { + WasmQuery::Smart { + contract_addr: _, + msg: _, + } => SystemResult::Ok(ContractResult::Ok( + to_binary(&token_info_response_copy).unwrap(), + )), + _ => SystemResult::Err(SystemError::UnsupportedRequest { + kind: "wasm".to_string(), + }), + }); + + // Populate token factory token's metadata from cw20 token's metadata + let tf_description = token_info_response.name.clone() + + ", " + + token_info_response.symbol.as_str() + + ", " + + tokenfactory_denom.as_str(); + let tf_denom_unit_base = DenomUnit { + denom: tokenfactory_denom.clone(), + exponent: 0, + aliases: vec![], + }; + let tf_scaled_denom = "wormhole/".to_string() + + subdenom.as_str() + + "/" + + token_info_response.decimals.to_string().as_str(); + let tf_denom_unit_scaled = DenomUnit { + denom: tf_scaled_denom, + exponent: u32::from(token_info_response.decimals), + aliases: vec![], + }; + let tf_metadata = Metadata { + description: Some(tf_description), + base: Some(tokenfactory_denom.clone()), + denom_units: vec![tf_denom_unit_base, tf_denom_unit_scaled], + display: Some(tokenfactory_denom.clone()), + name: Some(token_info_response.name), + symbol: Some(token_info_response.symbol), + }; + + let response = convert_cw20_to_bank_and_send( + deps.as_mut(), + env, + recipient, + amount, + contract_addr, + chain_id.into(), + None, + ) + .unwrap(); + + // response should have 3 messages: + assert_eq!(response.messages.len(), 3); + + let mut expected_response: Response = Response::new(); + expected_response = expected_response.add_message(TokenMsg::CreateDenom { + subdenom, + metadata: Some(tf_metadata), + }); + expected_response = expected_response.add_message(TokenMsg::MintTokens { + denom: tokenfactory_denom, + amount, + mint_to_address: "cosmos2contract".to_string(), + }); + expected_response = expected_response.add_message(Stargate { + type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), + value: Binary::from_base64("Cgh0cmFuc2ZlchIJY2hhbm5lbC0wGkkKRGZhY3RvcnkvY29zbW9zMmNvbnRyYWN0LzNRRVF5aTdpeUpId1E0d2ZVTUxGUEI0a1J6Y3pNQVhDaXRXaDdoNlRFVERhEgExIg9jb3Ntb3MyY29udHJhY3QqL3dvcm1ob2xlMXZoa20ycXY3ODRydWx4OHlscnUwenB2eXZ3M20zY3k5OWU2d3kwOL2i6MjOsZzqFQ==").unwrap(), + }); + + // 1. TokenMsg::CreateDenom + assert_eq!(response.messages[0].msg, expected_response.messages[0].msg,); + + // 2. TokenMsg::MintTokens + assert_eq!(response.messages[1].msg, expected_response.messages[1].msg,); + + // 3. Stargate ibc transfer + assert_eq!(response.messages[2].msg, expected_response.messages[2].msg,); +} + +// 3. Failure: couldn't validate contract address +#[test] +fn convert_cw20_to_bank_failure_invalid_contract() { + let mut deps = default_custom_mock_deps(); + let env = mock_env(); + let recipient = WORMHOLE_USER_ADDR.to_string(); + let amount = 1; + let contract_addr = "badContractAddr".to_string(); + let chain_id = Chain::Ethereum; + + let method_err = convert_cw20_to_bank_and_send( + deps.as_mut(), + env, + recipient, + amount, + contract_addr, + chain_id.into(), + None, + ) + .unwrap_err(); + assert_eq!( + method_err.to_string(), + "invalid contract address badContractAddr" + ); +} + +// 4. Failure: Chain id doesn't have a channel +#[test] +fn convert_cw20_to_bank_and_send_chain_id_no_channel() { + let mut deps = default_custom_mock_deps(); + let env = mock_env(); + let recipient = WORMHOLE_USER_ADDR.to_string(); + let amount = 1; + let contract_addr = WORMHOLE_CONTRACT_ADDR.to_string(); + + let tokenfactory_denom = + "factory/cosmos2contract/3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa".to_string(); + CW_DENOMS + .save( + deps.as_mut().storage, + contract_addr.clone(), + &tokenfactory_denom, + ) + .unwrap(); + + let chain_id = Chain::Ethereum; + + let method_err = convert_cw20_to_bank_and_send( + deps.as_mut(), + env, + recipient, + amount, + contract_addr, + chain_id.into(), + None, + ) + .unwrap_err(); + + assert_eq!( + method_err.to_string(), + "chain id does not have an allowed channel" + ); +} + +// 5. Failure: bad payload +#[test] +fn convert_cw20_to_bank_and_send_bad_payload() { + let mut deps = default_custom_mock_deps(); + let env = mock_env(); + let recipient = WORMHOLE_USER_ADDR.to_string(); + let amount = 1; + let contract_addr = WORMHOLE_CONTRACT_ADDR.to_string(); + + let tokenfactory_denom = + "factory/cosmos2contract/3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa".to_string(); + CW_DENOMS + .save( + deps.as_mut().storage, + contract_addr.clone(), + &tokenfactory_denom, + ) + .unwrap(); + + let chain_id = Chain::Ethereum; + let channel = "channel-0".to_string(); + CHAIN_TO_CHANNEL_MAP + .save(deps.as_mut().storage, chain_id.into(), &channel) + .unwrap(); + + let method_err = convert_cw20_to_bank_and_send( + deps.as_mut(), + env, + recipient, + amount, + contract_addr, + chain_id.into(), + Some( + Binary::from_base64("2VpMWV6czVtZG13OHd6dmNzOXZ4OXk3ZGt0cWdlM3l6bjR3MGw5bjQ0").unwrap(), + ), + ) + .unwrap_err(); + + assert_eq!( + method_err.to_string(), + "failed to convert 2VpMWV6czVtZG13OHd6dmNzOXZ4OXk3ZGt0cWdlM3l6bjR3MGw5bjQ0= to utf8 string" + ); +} + +// 1. happy path +#[test] +fn contract_addr_to_base58_happy_path() { + let deps = default_custom_mock_deps(); + let b58_str = contract_addr_to_base58( + deps.as_ref(), + "wormhole1yw4wv2zqg9xkn67zvq3azye0t8h0x9kgyg3d53jym24gxt49vdys6s8h7a".to_string(), + ) + .unwrap(); + assert_eq!(b58_str, "3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa"); +} + +// 2. bad contract address, could not canonicalize contract address +#[test] +fn contract_addr_to_base58_bad_contract_address() { + let deps = default_custom_mock_deps(); + let method_err = contract_addr_to_base58( + deps.as_ref(), + "wormhole1yw4wv2zqg9xkn67zvq3azye0t8h0x9kgyg3d53jym24gxt49vdys6s8h7".to_string(), + ) + .unwrap_err(); + assert_eq!( + method_err.to_string(), + "could not canonicalize contract address wormhole1yw4wv2zqg9xkn67zvq3azye0t8h0x9kgyg3d53jym24gxt49vdys6s8h7" + ) +} diff --git a/cosmwasm/contracts/ibc-translator/tests/test_setup/mod.rs b/cosmwasm/contracts/ibc-translator/tests/test_setup/mod.rs new file mode 100644 index 0000000000..2f8ac15efb --- /dev/null +++ b/cosmwasm/contracts/ibc-translator/tests/test_setup/mod.rs @@ -0,0 +1,349 @@ +use serde::de::DeserializeOwned; +use std::marker::PhantomData; + +use cosmwasm_std::{ + from_slice, + testing::{mock_env, BankQuerier, MockQuerierCustomHandlerResult, MockStorage}, + Addr, Api, Binary, CanonicalAddr, Coin, ContractResult, CustomQuery, Empty, Env, OwnedDeps, + Querier, QuerierResult, QueryRequest, RecoverPubkeyError, StdError, StdResult, SystemError, + SystemResult, VerificationError, WasmQuery, +}; +use wormhole_bindings::WormholeQuery; + +pub const WORMHOLE_CONTRACT_ADDR: &str = + "wormhole1yw4wv2zqg9xkn67zvq3azye0t8h0x9kgyg3d53jym24gxt49vdys6s8h7a"; +pub const WORMHOLE_USER_ADDR: &str = "wormhole1vhkm2qv784rulx8ylru0zpvyvw3m3cy99e6wy0"; +pub const WORMHOLE_CONTRACT_ADDR_BYTES: [u8; 32] = [ + 0x23, 0xaa, 0xe6, 0x28, 0x40, 0x41, 0x4d, 0x69, 0xeb, 0xc2, 0x60, 0x23, 0xd1, 0x13, 0x2f, 0x59, + 0xee, 0xf3, 0x16, 0xc8, 0x22, 0x22, 0xda, 0x46, 0x44, 0xda, 0xaa, 0x83, 0x2e, 0xa5, 0x63, 0x49, +]; +pub const WORMHOLE_USER_ADDR_BYTES: [u8; 20] = [ + 0x65, 0xed, 0xb5, 0x01, 0x9e, 0x3d, 0x47, 0xcf, 0x98, 0xe4, 0xf8, 0xf8, 0xf1, 0x05, 0x84, 0x63, + 0xa3, 0xb8, 0xe0, 0x85, +]; + +// Custom API mock implementation for testing. +// The custom impl helps us with correct addr_validate, addr_canonicalize, and addr_humanize methods for Wormchain. +#[derive(Clone)] +pub struct CustomApi { + contract_addr: String, + user_addr: String, + contract_addr_bin: Binary, + user_addr_bin: Binary, +} + +impl CustomApi { + pub fn new( + contract_addr: &str, + user_addr: &str, + contract_addr_bytes: [u8; 32], + user_addr_bytes: [u8; 20], + ) -> Self { + CustomApi { + contract_addr: contract_addr.to_string(), + user_addr: user_addr.to_string(), + contract_addr_bin: Binary::from(contract_addr_bytes), + user_addr_bin: Binary::from(user_addr_bytes), + } + } +} + +impl Api for CustomApi { + fn addr_validate(&self, input: &str) -> StdResult { + if input == self.contract_addr { + return Ok(Addr::unchecked(self.contract_addr.clone())); + } + + if input == self.user_addr { + return Ok(Addr::unchecked(self.user_addr.clone())); + } + + Err(StdError::GenericErr { + msg: "case not found".to_string(), + }) + } + + fn addr_canonicalize(&self, input: &str) -> StdResult { + if input == self.contract_addr { + return Ok(CanonicalAddr(self.contract_addr_bin.clone())); + } + + if input == self.user_addr { + return Ok(CanonicalAddr(self.user_addr_bin.clone())); + } + + Err(StdError::GenericErr { + msg: "case not found".to_string(), + }) + } + + fn addr_humanize(&self, canonical: &CanonicalAddr) -> StdResult { + if *canonical == self.contract_addr_bin { + return Ok(Addr::unchecked(self.contract_addr.clone())); + } + + if *canonical == self.user_addr_bin { + return Ok(Addr::unchecked(self.user_addr.clone())); + } + + Err(StdError::GenericErr { + msg: "case not found".to_string(), + }) + } + + fn secp256k1_verify( + &self, + message_hash: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result { + Ok(cosmwasm_crypto::secp256k1_verify( + message_hash, + signature, + public_key, + )?) + } + + fn secp256k1_recover_pubkey( + &self, + message_hash: &[u8], + signature: &[u8], + recovery_param: u8, + ) -> Result, RecoverPubkeyError> { + let pubkey = + cosmwasm_crypto::secp256k1_recover_pubkey(message_hash, signature, recovery_param)?; + Ok(pubkey.to_vec()) + } + + fn ed25519_verify( + &self, + message: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result { + Ok(cosmwasm_crypto::ed25519_verify( + message, signature, public_key, + )?) + } + + fn ed25519_batch_verify( + &self, + messages: &[&[u8]], + signatures: &[&[u8]], + public_keys: &[&[u8]], + ) -> Result { + Ok(cosmwasm_crypto::ed25519_batch_verify( + messages, + signatures, + public_keys, + )?) + } + + fn debug(&self, message: &str) { + println!("{message}"); + } +} + +#[allow(dead_code)] +pub fn default_custom_mock_deps() -> OwnedDeps { + OwnedDeps { + storage: MockStorage::default(), + api: CustomApi::new( + WORMHOLE_CONTRACT_ADDR, + WORMHOLE_USER_ADDR, + WORMHOLE_CONTRACT_ADDR_BYTES, + WORMHOLE_USER_ADDR_BYTES, + ), + querier: MockQuerier::default(), + custom_query_type: PhantomData, + } +} + +#[allow(dead_code)] +pub fn execute_custom_mock_deps() -> OwnedDeps { + OwnedDeps { + storage: MockStorage::default(), + api: CustomApi::new( + WORMHOLE_CONTRACT_ADDR, + WORMHOLE_USER_ADDR, + WORMHOLE_CONTRACT_ADDR_BYTES, + WORMHOLE_USER_ADDR_BYTES, + ), + querier: MockQuerier::default(), + custom_query_type: PhantomData, + } +} + +#[allow(dead_code)] +pub fn mock_env_custom_contract(contract_addr: impl Into) -> Env { + let mut env = mock_env(); + env.contract.address = Addr::unchecked(contract_addr); + env +} + +/// MockQuerier holds an immutable table of bank balances +/// and configurable handlers for Wasm queries and custom queries. +pub struct MockQuerier { + bank: BankQuerier, + #[cfg(feature = "staking")] + staking: StakingQuerier, + wasm: WasmQuerier, + #[cfg(feature = "stargate")] + ibc: IbcQuerier, + /// A handler to handle custom queries. This is set to a dummy handler that + /// always errors by default. Update it via `with_custom_handler`. + /// + /// Use box to avoid the need of another generic type + custom_handler: Box Fn(&'a C) -> MockQuerierCustomHandlerResult>, +} + +impl MockQuerier { + pub fn new(balances: &[(&str, &[Coin])]) -> Self { + MockQuerier { + bank: BankQuerier::new(balances), + #[cfg(feature = "staking")] + staking: StakingQuerier::default(), + wasm: WasmQuerier::default(), + #[cfg(feature = "stargate")] + ibc: IbcQuerier::default(), + // strange argument notation suggested as a workaround here: https://github.com/rust-lang/rust/issues/41078#issuecomment-294296365 + custom_handler: Box::from(|_: &_| -> MockQuerierCustomHandlerResult { + SystemResult::Ok(ContractResult::Ok(Binary::from_base64("e30=").unwrap())) + }), + } + } + + // set a new balance for the given address and return the old balance + #[allow(dead_code)] + pub fn update_balance( + &mut self, + addr: impl Into, + balance: Vec, + ) -> Option> { + self.bank.update_balance(addr, balance) + } + + #[cfg(feature = "staking")] + pub fn update_staking( + &mut self, + denom: &str, + validators: &[crate::query::Validator], + delegations: &[crate::query::FullDelegation], + ) { + self.staking = StakingQuerier::new(denom, validators, delegations); + } + + #[cfg(feature = "stargate")] + pub fn update_ibc(&mut self, port_id: &str, channels: &[IbcChannel]) { + self.ibc = IbcQuerier::new(port_id, channels); + } + + pub fn update_wasm(&mut self, handler: WH) + where + WH: Fn(&WasmQuery) -> QuerierResult, + { + self.wasm.update_handler(handler) + } + + #[allow(dead_code)] + pub fn with_custom_handler(mut self, handler: CH) -> Self + where + CH: Fn(&C) -> MockQuerierCustomHandlerResult, + { + self.custom_handler = Box::from(handler); + self + } +} + +impl Default for MockQuerier { + fn default() -> Self { + MockQuerier::new(&[]) + } +} + +impl Querier for MockQuerier { + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { + let request: QueryRequest = match from_slice(bin_request) { + Ok(v) => v, + Err(e) => { + return SystemResult::Err(SystemError::InvalidRequest { + error: format!("Parsing query request: {e}"), + request: bin_request.into(), + }) + } + }; + self.handle_query(&request) + } +} + +impl MockQuerier { + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { + match &request { + QueryRequest::Bank(bank_query) => self.bank.query(bank_query), + QueryRequest::Custom(custom_query) => (*self.custom_handler)(custom_query), + #[cfg(feature = "staking")] + QueryRequest::Staking(staking_query) => self.staking.query(staking_query), + QueryRequest::Wasm(msg) => self.wasm.query(msg), + #[cfg(feature = "stargate")] + QueryRequest::Stargate { .. } => SystemResult::Err(SystemError::UnsupportedRequest { + kind: "Stargate".to_string(), + }), + #[cfg(feature = "stargate")] + QueryRequest::Ibc(msg) => self.ibc.query(msg), + //_ => SystemResult::Err(SystemError::UnsupportedRequest { + // kind: "Unknown".to_string(), + //}), + _ => SystemResult::Ok(ContractResult::Ok(Binary::default())), + } + } +} + +struct WasmQuerier { + /// A handler to handle Wasm queries. This is set to a dummy handler that + /// always errors by default. Update it via `with_custom_handler`. + /// + /// Use box to avoid the need of generic type. + handler: Box Fn(&'a WasmQuery) -> QuerierResult>, +} + +impl WasmQuerier { + fn new(handler: Box Fn(&'a WasmQuery) -> QuerierResult>) -> Self { + Self { handler } + } + + fn update_handler(&mut self, handler: WH) + where + WH: Fn(&WasmQuery) -> QuerierResult, + { + self.handler = Box::from(handler) + } + + fn query(&self, request: &WasmQuery) -> QuerierResult { + (*self.handler)(request) + } +} + +impl Default for WasmQuerier { + fn default() -> Self { + let handler = Box::from(|request: &WasmQuery| -> QuerierResult { + let err = match request { + WasmQuery::Smart { contract_addr, .. } => SystemError::NoSuchContract { + addr: contract_addr.clone(), + }, + WasmQuery::Raw { contract_addr, .. } => SystemError::NoSuchContract { + addr: contract_addr.clone(), + }, + WasmQuery::ContractInfo { contract_addr, .. } => SystemError::NoSuchContract { + addr: contract_addr.clone(), + }, + #[cfg(feature = "cosmwasm_1_2")] + WasmQuery::CodeInfo { code_id, .. } => { + SystemError::NoSuchCode { code_id: *code_id } + } + _ => SystemError::Unknown {}, + }; + SystemResult::Err(err) + }); + Self::new(handler) + } +}