Эх сурвалжийг харах

cosmwasm: added shutdown contracts tests (#4257)

* cosmwasm: added shutdown contracts tests

* updated workflow settings

* cosmwasm: added allow dead_code tags for shutdown mode contracts

* cosmwasm: added shutdown contracts tests

* updated workflow settings

* cosmwasm: added allow dead_code tags for shutdown mode contracts

* sdk: update vaa-serde as per clippy

https://github.com/wormhole-foundation/wormhole/actions/runs/14248127718/job/39947492191?pr=4257
Kaku 7 сар өмнө
parent
commit
7349f6ad7e

+ 3 - 1
.github/workflows/build.yml

@@ -347,7 +347,9 @@ jobs:
       matrix:
         manifest:
           - path: cosmwasm/Cargo.toml
-            args: "--workspace --locked"
+            args: "--workspace --locked --exclude 'shutdown-*'"
+          - path: cosmwasm/Cargo.toml
+            args: "-p 'shutdown-*' --no-default-features --locked"
           - path: terra/Cargo.toml
             args: "--workspace --locked"
           - path: sdk/rust/Cargo.toml

+ 1 - 0
.github/workflows/wormchain-icts.yml

@@ -67,6 +67,7 @@ jobs:
           # - "ictest-ibc-receiver"
           - "ictest-validator-hotswap"
           - "ictest-cw-wormhole"
+          - "ictest-cw-shutdown-contracts"
       fail-fast: false
 
     steps:

+ 11 - 0
cosmwasm/Cargo.lock

@@ -1862,7 +1862,18 @@ dependencies = [
 name = "shutdown-core-bridge-cosmwasm"
 version = "0.1.0"
 dependencies = [
+ "cosmwasm-std",
+ "cw-multi-test",
+ "hex",
+ "k256",
+ "serde",
+ "serde-json-wasm 0.4.1",
+ "serde_wormhole",
+ "tiny-keccak",
+ "wormchain-ibc-receiver",
+ "wormhole-bindings",
  "wormhole-cosmwasm",
+ "wormhole-vaas-serde",
 ]
 
 [[package]]

+ 3 - 0
cosmwasm/Cargo.toml

@@ -31,6 +31,9 @@ panic = 'abort'
 incremental = false
 overflow-checks = true
 
+[workspace.package]
+shutdown-core-bridge-cosmwasm = { path = "contracts/shutdown-wormhole", default-features = false}
+
 [workspace.dependencies.serde_wormhole]
 version = "0.1.0"
 

+ 2 - 0
cosmwasm/Dockerfile

@@ -12,6 +12,8 @@ COPY cosmwasm/artifacts /code/artifacts
 COPY sdk/rust /sdk/rust
 
 RUN --mount=type=cache,target=/target,id=cosmwasm_target --mount=type=cache,target=/usr/local/cargo/registry optimize.sh .
+# Build only shutdown contracts with no default features
+RUN --mount=type=cache,target=/target,id=cosmwasm_target --mount=type=cache,target=/usr/local/cargo/registry optimize.sh ./contracts/shutdown-wormhole
 
 FROM scratch as artifacts
 COPY --from=builder /code/artifacts /

+ 2 - 1
cosmwasm/Makefile

@@ -17,7 +17,8 @@ tools/node_modules: tools/package-lock.json
 .PHONY: test
 ## Run unit tests
 test:
-	cargo test --workspace --locked
+	cargo test --workspace --locked --exclude 'shutdown-*'
+	cargo test -p 'shutdown-*' --no-default-features --locked
 
 
 .PHONY: clean

+ 13 - 0
cosmwasm/contracts/shutdown-wormhole/Cargo.toml

@@ -10,3 +10,16 @@ crate-type = ["cdylib", "rlib"]
 
 [dependencies]
 wormhole-cosmwasm = { version = "0.1.0", default-features = false }
+
+[dev-dependencies]
+cw-multi-test = "0.14"
+serde_wormhole.workspace = true
+wormhole-sdk.workspace = true
+wormhole-bindings = { version = "0.1", features = ["fake"] }
+tiny-keccak = { version = "2.0", features = ["keccak"] }
+serde-json-wasm = "0.4"
+wormchain-ibc-receiver = { path = "../wormchain-ibc-receiver" }
+hex = "0.4.2"
+serde = { version = "1.0.137", default-features = false, features = ["derive"] }
+k256 = { version = "0.11", default-features = false, features = ["ecdsa"] }
+cosmwasm-std = { version = "1.0.0" }

+ 2 - 0
cosmwasm/contracts/shutdown-wormhole/src/lib.rs

@@ -1 +1,3 @@
 pub use cw_wormhole::contract;
+#[cfg(test)]
+mod testing;

+ 248 - 0
cosmwasm/contracts/shutdown-wormhole/src/testing/integration.rs

@@ -0,0 +1,248 @@
+use crate::testing::utils::{IntoGuardianAddress, WormholeApp};
+use cosmwasm_std::StdResult;
+use cosmwasm_std::Uint256;
+use cw_multi_test::Executor;
+use cw_wormhole::{
+    msg::QueryMsg,
+    msg::{ExecuteMsg, GuardianSetInfoResponse},
+    state::ParsedVAA,
+};
+use k256::ecdsa::SigningKey;
+use wormhole_bindings::fake::{create_gov_vaa_body, SignVaa};
+use wormhole_sdk::{
+    core::{Action, GovernancePacket},
+    Address, Amount, Chain, GuardianSetInfo,
+};
+
+#[test]
+fn post_message_blocked_in_shutdown() {
+    let WormholeApp {
+        mut app,
+        wormhole_contract,
+        user,
+        ..
+    } = WormholeApp::new_with_faker_guardians();
+
+    // Attempt to post a message - should fail due to shutdown
+    let post_message_response = app.execute_contract(
+        user.clone(),
+        wormhole_contract.clone(),
+        &ExecuteMsg::PostMessage {
+            message: b"test".into(),
+            nonce: 2,
+        },
+        &[],
+    );
+
+    assert!(
+        post_message_response.is_err(),
+        "Post Message should fail in shutdown mode with \"ContractShutdown\""
+    );
+}
+
+#[test]
+fn fee_change_blocked_in_shutdown() -> StdResult<()> {
+    let WormholeApp {
+        mut app,
+        wormhole_contract,
+        wormhole_keeper,
+        user,
+        ..
+    } = WormholeApp::new_with_guardians(vec![SigningKey::from_bytes(&[
+        93, 217, 189, 224, 168, 81, 157, 93, 238, 38, 143, 8, 182, 94, 69, 77, 232, 199, 238, 206,
+        15, 135, 221, 58, 43, 74, 0, 129, 54, 198, 62, 226,
+    ])
+    .unwrap()]);
+
+    let vaa_body = create_gov_vaa_body(
+        2,
+        GovernancePacket {
+            chain: Chain::Terra2,
+            action: Action::SetFee {
+                amount: Amount(Uint256::from(18u128).to_be_bytes()),
+            },
+        },
+    );
+
+    let (_, signed_vaa) = vaa_body.clone().sign_vaa(&wormhole_keeper);
+
+    // Attempt to submit fee change VAA - should fail due to shutdown
+    let vaa_response = app.execute_contract(
+        user.clone(),
+        wormhole_contract.clone(),
+        &ExecuteMsg::SubmitVAA {
+            vaa: signed_vaa.clone(),
+        },
+        &[],
+    );
+    println!("vaa resp {:?}", vaa_response);
+
+    assert!(
+        vaa_response.is_err(),
+        "SetFee VAA submission should fail in shutdown mode with \"ContractShutdown\""
+    );
+
+    Ok(())
+}
+
+#[test]
+fn transfer_fee_blocked_in_shutdown() -> StdResult<()> {
+    let WormholeApp {
+        mut app,
+        wormhole_contract,
+        wormhole_keeper,
+        user,
+        ..
+    } = WormholeApp::new_with_guardians(vec![SigningKey::from_bytes(&[
+        93, 217, 189, 224, 168, 81, 157, 93, 238, 38, 143, 8, 182, 94, 69, 77, 232, 199, 238, 206,
+        15, 135, 221, 58, 43, 74, 0, 129, 54, 198, 62, 226,
+    ])
+    .unwrap()]);
+
+    let vaa_body = create_gov_vaa_body(
+        2,
+        GovernancePacket {
+            chain: Chain::Terra2,
+            action: Action::TransferFee {
+                amount: Amount(Uint256::from(100u128).to_be_bytes()),
+                recipient: Address([1u8; 32]),
+            },
+        },
+    );
+
+    let (_, signed_vaa) = vaa_body.clone().sign_vaa(&wormhole_keeper);
+
+    // Attempt to submit transfer fee VAA - should fail due to shutdown
+    let vaa_response = app.execute_contract(
+        user.clone(),
+        wormhole_contract.clone(),
+        &ExecuteMsg::SubmitVAA {
+            vaa: signed_vaa.clone(),
+        },
+        &[],
+    );
+
+    assert!(
+        vaa_response.is_err(),
+        "TransferFee VAA submission should fail in shutdown mode with \"ContractShutdown\""
+    );
+
+    Ok(())
+}
+
+#[test]
+pub fn guardian_set_update_allowed_in_shutdown() -> StdResult<()> {
+    let WormholeApp {
+        mut app,
+        wormhole_contract,
+        wormhole_keeper,
+        user,
+        ..
+    } = WormholeApp::new_with_guardians(vec![SigningKey::from_bytes(&[
+        93, 217, 189, 224, 168, 81, 157, 93, 238, 38, 143, 8, 182, 94, 69, 77, 232, 199, 238, 206,
+        15, 135, 221, 58, 43, 74, 0, 129, 54, 198, 62, 226,
+    ])
+    .unwrap()]);
+
+    // Query the current guardian set
+    let guardian_set_response: GuardianSetInfoResponse = app
+        .wrap()
+        .query_wasm_smart(wormhole_contract.clone(), &QueryMsg::GuardianSetInfo {})?;
+
+    let new_guardian_keys = vec![
+        SigningKey::from_bytes(&[
+            93, 217, 189, 224, 168, 81, 157, 93, 238, 38, 143, 8, 182, 94, 69, 77, 232, 199, 238,
+            206, 15, 135, 221, 58, 43, 74, 0, 129, 54, 198, 62, 226,
+        ])
+        .unwrap(),
+        SigningKey::from_bytes(&[
+            150, 48, 135, 223, 194, 186, 243, 139, 177, 8, 126, 32, 210, 57, 42, 28, 29, 102, 196,
+            201, 106, 136, 40, 149, 218, 150, 240, 213, 192, 128, 161, 245,
+        ])
+        .unwrap(),
+    ];
+
+    let update_guardian_set_vaa_body = create_gov_vaa_body(
+        2,
+        GovernancePacket {
+            chain: Chain::Terra2,
+            action: Action::GuardianSetUpgrade {
+                new_guardian_set_index: guardian_set_response.guardian_set_index + 1,
+                new_guardian_set: GuardianSetInfo {
+                    addresses: new_guardian_keys
+                        .iter()
+                        .map(|key| -> wormhole_sdk::GuardianAddress {
+                            key.clone().into_guardian_address()
+                        })
+                        .collect(),
+                },
+            },
+        },
+    );
+
+    let (_, signed_guardian_set_update_vaa) = update_guardian_set_vaa_body
+        .clone()
+        .sign_vaa(&wormhole_keeper);
+
+    // Submit guardian set update VAA - should succeed despite shutdown
+    let guardian_set_update_response = app.execute_contract(
+        user.clone(),
+        wormhole_contract.clone(),
+        &ExecuteMsg::SubmitVAA {
+            vaa: signed_guardian_set_update_vaa.clone(),
+        },
+        &[],
+    );
+
+    assert!(
+        guardian_set_update_response.is_ok(),
+        "Guardian set update should succeed even in shutdown mode"
+    );
+
+    // Verify the guardian set was actually updated
+    let new_guardian_set_response: GuardianSetInfoResponse = app
+        .wrap()
+        .query_wasm_smart(wormhole_contract.clone(), &QueryMsg::GuardianSetInfo {})?;
+
+    assert_eq!(
+        new_guardian_set_response.guardian_set_index,
+        guardian_set_response.guardian_set_index + 1,
+        "Guardian set index should be incremented"
+    );
+    assert_eq!(
+        new_guardian_set_response.addresses.len(),
+        2,
+        "New guardian set should have 2 guardians"
+    );
+
+    Ok(())
+}
+
+#[test]
+pub fn verify_vaa_allowed_in_shutdown() -> StdResult<()> {
+    let WormholeApp {
+        app,
+        wormhole_contract,
+        wormhole_keeper,
+        ..
+    } = WormholeApp::new_with_faker_guardians();
+
+    let (_, signed_vaa) = create_gov_vaa_body(1, "test").sign_vaa(&wormhole_keeper);
+
+    // Query verify VAA - should work despite shutdown
+    let parsed_vaa: ParsedVAA = app.wrap().query_wasm_smart(
+        wormhole_contract.clone(),
+        &QueryMsg::VerifyVAA {
+            vaa: signed_vaa,
+            block_time: app.block_info().height,
+        },
+    )?;
+
+    assert_eq!(parsed_vaa.version, 1, "version should match");
+    assert_eq!(
+        parsed_vaa.guardian_set_index, 0,
+        "guardian set index should match"
+    );
+
+    Ok(())
+}

+ 2 - 0
cosmwasm/contracts/shutdown-wormhole/src/testing/mod.rs

@@ -0,0 +1,2 @@
+mod integration;
+mod utils;

+ 150 - 0
cosmwasm/contracts/shutdown-wormhole/src/testing/utils.rs

@@ -0,0 +1,150 @@
+use cosmwasm_std::Addr;
+use cw_multi_test::{App, AppBuilder, ContractWrapper, Executor, WasmKeeper};
+use cw_wormhole::{
+    contract::{execute, instantiate, query},
+    msg::InstantiateMsg,
+    state::{GuardianAddress, GuardianSetInfo},
+};
+use k256::ecdsa::SigningKey;
+use k256::elliptic_curve::sec1::ToEncodedPoint;
+use std::convert::TryInto;
+use tiny_keccak::{Hasher, Keccak};
+use wormhole_bindings::fake::{default_guardian_keys, WormholeKeeper};
+use wormhole_sdk::{Chain, GOVERNANCE_EMITTER};
+
+pub struct WormholeApp {
+    pub app: App<
+        cw_multi_test::BankKeeper,
+        cosmwasm_std::testing::MockApi,
+        cosmwasm_std::MemoryStorage,
+        WormholeKeeper,
+        WasmKeeper<cosmwasm_std::Empty, wormhole_bindings::WormholeQuery>,
+    >,
+    pub user: Addr,
+    pub wormhole_contract: Addr,
+    pub wormhole_keeper: WormholeKeeper,
+}
+
+impl WormholeApp {
+    pub fn new_with_guardians(guardians: Vec<SigningKey>) -> Self {
+        create_wormhole_app(Some((
+            instantiate_with_guardians(
+                guardians
+                    .iter()
+                    .map(key_to_guardian_address)
+                    .collect::<Vec<GuardianAddress>>()
+                    .as_slice(),
+            ),
+            guardians,
+        )))
+    }
+    pub fn new_with_faker_guardians() -> Self {
+        create_wormhole_app(Some((
+            instantiate_with_guardians(
+                default_guardian_keys()
+                    .iter()
+                    .map(key_to_guardian_address)
+                    .collect::<Vec<GuardianAddress>>()
+                    .as_slice(),
+            ),
+            default_guardian_keys().to_vec(),
+        )))
+    }
+}
+
+pub fn instantiate_with_guardians(guardians: &[GuardianAddress]) -> InstantiateMsg {
+    InstantiateMsg {
+        gov_chain: Chain::Solana.into(),
+        gov_address: GOVERNANCE_EMITTER.0.into(),
+        initial_guardian_set: GuardianSetInfo {
+            addresses: guardians.to_vec(),
+            expiration_time: 1571797500,
+        },
+        guardian_set_expirity: 50,
+        chain_id: Chain::Terra2.into(),
+        fee_denom: "uluna".to_string(),
+    }
+}
+
+pub fn create_wormhole_app(
+    instantiate_msg: Option<(InstantiateMsg, Vec<SigningKey>)>,
+) -> WormholeApp {
+    let (instantiate_msg, keys) = instantiate_msg.unwrap_or_else(|| {
+        let key_bytes =
+            hex::decode("beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe").expect("Decoding failed");
+        (
+            instantiate_with_guardians(&[GuardianAddress {
+                bytes: key_bytes.clone().into(),
+            }]),
+            vec![SigningKey::from_bytes(key_bytes.as_slice()).unwrap()],
+        )
+    });
+
+    let wormhole_keeper: WormholeKeeper = keys.to_vec().into();
+
+    let mut app = AppBuilder::new_custom()
+        .with_custom(wormhole_keeper.clone())
+        .build(|_, _, _| {});
+
+    let admin = Addr::unchecked("admin");
+    let user = Addr::unchecked("user");
+
+    let cw_wormhole_wrapper = ContractWrapper::new_with_empty(execute, instantiate, query);
+
+    let code_id = app.store_code(Box::new(cw_wormhole_wrapper));
+
+    let contract_addr = app
+        .instantiate_contract(
+            code_id,
+            admin.clone(),
+            &instantiate_msg,
+            &[],
+            "cw_wormhole",
+            Some(admin.to_string()),
+        )
+        .unwrap();
+
+    WormholeApp {
+        app,
+        user,
+        wormhole_contract: contract_addr,
+        wormhole_keeper,
+    }
+}
+
+pub fn key_to_guardian_address(value: &SigningKey) -> GuardianAddress {
+    // Get the public key bytes
+    let public_key = value.verifying_key().to_encoded_point(false);
+    let public_key_bytes = public_key.as_bytes();
+
+    // Skip the first byte (0x04 prefix for uncompressed public keys)
+    let key_without_prefix = &public_key_bytes[1..];
+
+    // Hash with Keccak-256
+    let mut hasher = Keccak::v256();
+    let mut hash = [0u8; 32];
+    hasher.update(key_without_prefix);
+    hasher.finalize(&mut hash);
+
+    // Take last 20 bytes
+    let address = &hash[12..32];
+
+    GuardianAddress {
+        bytes: address.to_vec().into(),
+    }
+}
+
+pub trait IntoGuardianAddress {
+    fn into_guardian_address(self) -> wormhole_sdk::GuardianAddress;
+}
+
+impl IntoGuardianAddress for SigningKey {
+    fn into_guardian_address(self) -> wormhole_sdk::GuardianAddress {
+        let guardian: GuardianAddress = key_to_guardian_address(&self);
+
+        // Take last 20 bytes
+        let address: [u8; 20] = guardian.bytes.0.try_into().unwrap();
+
+        wormhole_sdk::GuardianAddress(address)
+    }
+}

+ 17 - 1
cosmwasm/contracts/token-bridge/src/contract.rs

@@ -44,6 +44,7 @@ use crate::{
     token_address::{ContractId, ExternalTokenId, TokenId, WrappedCW20},
 };
 
+#[cfg_attr(not(feature = "full"), allow(dead_code))]
 type HumanAddr = String;
 
 pub enum TransferType<A> {
@@ -312,6 +313,7 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S
     }
 }
 
+#[cfg_attr(not(feature = "full"), allow(dead_code))]
 fn deposit_tokens(deps: DepsMut, _env: Env, info: MessageInfo) -> StdResult<Response> {
     for coin in info.funds {
         let deposit_key = format!("{}:{}", info.sender, coin.denom);
@@ -326,6 +328,7 @@ fn deposit_tokens(deps: DepsMut, _env: Env, info: MessageInfo) -> StdResult<Resp
     Ok(Response::new().add_attribute("action", "deposit_tokens"))
 }
 
+#[cfg_attr(not(feature = "full"), allow(dead_code))]
 fn withdraw_tokens(
     deps: DepsMut,
     _env: Env,
@@ -358,6 +361,7 @@ fn withdraw_tokens(
 }
 
 /// Handle wrapped asset registration messages
+#[cfg_attr(not(feature = "full"), allow(dead_code))]
 fn handle_register_asset(
     deps: DepsMut,
     _env: Env,
@@ -400,6 +404,7 @@ fn handle_register_asset(
         .add_attribute("contract_addr", info.sender))
 }
 
+#[cfg_attr(not(feature = "full"), allow(dead_code))]
 fn handle_attest_meta(
     deps: DepsMut,
     env: Env,
@@ -487,6 +492,7 @@ fn handle_attest_meta(
     Ok(Response::new().add_message(message))
 }
 
+#[cfg_attr(not(feature = "full"), allow(dead_code))]
 fn handle_create_asset_meta(
     deps: DepsMut,
     env: Env,
@@ -504,6 +510,7 @@ fn handle_create_asset_meta(
     }
 }
 
+#[cfg_attr(not(feature = "full"), allow(dead_code))]
 fn handle_create_asset_meta_token(
     deps: DepsMut,
     env: Env,
@@ -554,6 +561,7 @@ fn handle_create_asset_meta_token(
         .add_attribute("meta.block_time", env.block.time.seconds().to_string()))
 }
 
+#[cfg_attr(not(feature = "full"), allow(dead_code))]
 fn handle_create_asset_meta_native_token(
     deps: DepsMut,
     env: Env,
@@ -598,6 +606,7 @@ fn handle_create_asset_meta_native_token(
         .add_attribute("meta.block_time", env.block.time.seconds().to_string()))
 }
 
+#[cfg_attr(not(feature = "full"), allow(dead_code))]
 fn handle_complete_transfer_with_payload(
     mut deps: DepsMut,
     env: Env,
@@ -658,9 +667,10 @@ fn parse_and_archive_vaa(
 fn submit_vaa(
     mut deps: DepsMut,
     env: Env,
-    info: MessageInfo,
+    #[cfg_attr(not(feature = "full"), allow(unused_variables))] info: MessageInfo,
     data: &Binary,
 ) -> StdResult<Response> {
+    #[cfg_attr(not(feature = "full"), allow(unused_variables))]
     let (vaa, payload) = parse_and_archive_vaa(deps.branch(), env.clone(), data)?;
     match payload {
         Either::Left(governance_packet) => handle_governance_payload(deps, env, &governance_packet),
@@ -756,6 +766,7 @@ fn handle_register_chain(deps: DepsMut, _env: Env, data: &Vec<u8>) -> StdResult<
 }
 
 #[allow(clippy::too_many_arguments)]
+#[cfg_attr(not(feature = "full"), allow(dead_code))]
 fn handle_complete_transfer(
     deps: DepsMut,
     env: Env,
@@ -798,6 +809,7 @@ fn handle_complete_transfer(
 
 #[allow(clippy::too_many_arguments)]
 #[allow(clippy::bind_instead_of_map)]
+#[cfg_attr(not(feature = "full"), allow(dead_code))]
 fn handle_complete_transfer_token(
     deps: DepsMut,
     _env: Env,
@@ -968,6 +980,7 @@ fn handle_complete_transfer_token(
 }
 
 #[allow(clippy::too_many_arguments)]
+#[cfg_attr(not(feature = "full"), allow(dead_code))]
 fn handle_complete_transfer_token_native(
     deps: DepsMut,
     _env: Env,
@@ -1067,6 +1080,7 @@ fn handle_complete_transfer_token_native(
 }
 
 #[allow(clippy::too_many_arguments)]
+#[cfg_attr(not(feature = "full"), allow(dead_code))]
 fn handle_initiate_transfer(
     deps: DepsMut,
     env: Env,
@@ -1107,6 +1121,7 @@ fn handle_initiate_transfer(
 }
 
 #[allow(clippy::too_many_arguments)]
+#[cfg_attr(not(feature = "full"), allow(dead_code))]
 fn handle_initiate_transfer_token(
     deps: DepsMut,
     env: Env,
@@ -1347,6 +1362,7 @@ fn handle_initiate_transfer_token(
 }
 
 #[allow(clippy::too_many_arguments)]
+#[cfg_attr(not(feature = "full"), allow(dead_code))]
 fn handle_initiate_transfer_native_token(
     deps: DepsMut,
     env: Env,

+ 1 - 0
cosmwasm/contracts/wormhole/src/contract.rs

@@ -302,6 +302,7 @@ pub fn handle_transfer_fee(deps: DepsMut, _env: Env, data: &[u8]) -> StdResult<R
     })))
 }
 
+#[cfg_attr(not(feature = "full"), allow(dead_code))]
 fn handle_post_message(
     deps: DepsMut,
     env: Env,

+ 8 - 0
wormchain/Makefile

@@ -124,6 +124,14 @@ ictest-cw-wormhole: rm-testcache
 	cd interchaintest && go test -race -v -run ^TestCWWormhole ./...
 .PHONY: ictest-cw-wormhole
 
+ictest-cw-shutdown-contracts: rm-testcache
+	cd interchaintest && go test -race -v -run ^TestShutdown ./...
+.PHONY: ictest-cw-shutdown-contracts
+
+ictest-cw-shutdown-contracts: rm-testcache
+	cd interchaintest && go test -race -v -run ^TestShutdown ./...
+
 ictest-validator-hotswap: rm-testcache
 	cd interchaintest && go test -race -v -run ^TestValidatorHotswap$$ ./...
 .PHONY: ictest-validator-hotswap
+

BIN
wormchain/interchaintest/contracts/shutdown_token_bridge.wasm


BIN
wormchain/interchaintest/contracts/shutdown_wormhole_core.wasm


+ 254 - 0
wormchain/interchaintest/shutdown_contracts_test.go

@@ -0,0 +1,254 @@
+package ictest
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"testing"
+
+	sdk "github.com/cosmos/cosmos-sdk/types"
+	"github.com/strangelove-ventures/interchaintest/v4"
+	"github.com/strangelove-ventures/interchaintest/v4/chain/cosmos"
+	"github.com/strangelove-ventures/interchaintest/v4/ibc"
+	"github.com/stretchr/testify/require"
+	"github.com/wormhole-foundation/wormchain/interchaintest/guardians"
+	"github.com/wormhole-foundation/wormchain/interchaintest/helpers"
+	"github.com/wormhole-foundation/wormchain/interchaintest/helpers/cw_wormhole"
+	"github.com/wormhole-foundation/wormhole/sdk/vaa"
+)
+
+// TestShutdownCoreContract tests the endpoints of a contract in its "shutdown" state
+func TestShutdownCoreContract(t *testing.T) {
+	// Setup chain and contract
+	numVals := 1
+	oldGuardians := guardians.CreateValSet(t, numVals)
+	chain := createSingleNodeCluster(t, "v2.24.2", *oldGuardians)
+	ctx, _ := buildSingleNodeInterchain(t, chain)
+
+	// Chains
+	wormchain := chain.(*cosmos.CosmosChain)
+
+	// Users
+	users := interchaintest.GetAndFundTestUsers(t, ctx, t.Name(), 1, wormchain)
+	user := users[0]
+
+	// Deploy contract to wormhole
+	coreInstantiateMsg := helpers.CoreContractInstantiateMsg(t, wormchainConfig, vaa.ChainIDWormchain, oldGuardians)
+	contractInfo := helpers.StoreAndInstantiateWormholeContract(t, ctx, wormchain, "faucet", "./contracts/wormhole_core.wasm", "wormhole_core", coreInstantiateMsg, oldGuardians)
+	contractAddr := contractInfo.Address
+
+	// Store a new version of the contract to upgrade to
+	wormNewCodeId := helpers.StoreContract(t, ctx, wormchain, "faucet", "./contracts/shutdown_wormhole_core.wasm", oldGuardians)
+
+	// Submit contract upgrade
+	err := cw_wormhole.SubmitContractUpgrade(t, ctx, oldGuardians, wormchain, contractAddr, wormNewCodeId)
+	require.NoError(t, err)
+
+	// -----------------------------------
+
+	// Try to post a message - should fail
+	message := []byte("test message")
+	messageBase64 := base64.StdEncoding.EncodeToString(message)
+	nonce := 1
+
+	executeMsg, err := json.Marshal(cw_wormhole.ExecuteMsg{
+		PostMessage: &cw_wormhole.ExecuteMsg_PostMessage{
+			Message: cw_wormhole.Binary(messageBase64),
+			Nonce:   nonce,
+		},
+	})
+	require.NoError(t, err)
+
+	// Execute contract
+	_, err = wormchain.ExecuteContract(ctx, "faucet", contractAddr, string(executeMsg))
+	require.Error(t, err)
+
+	// -----------------------------------
+
+	// Try to set fees - should fail
+	_, err = cw_wormhole.SubmitFeeUpdate(t, ctx, oldGuardians, wormchain, contractAddr, "1000000", false)
+	require.Error(t, err)
+
+	// -----------------------------------
+
+	// Try to transfer fees - should fail
+	_, err = cw_wormhole.SubmitTransferFee(t, ctx, oldGuardians, wormchain, contractAddr, []byte(user.Address), "10000000000", false)
+	require.Error(t, err)
+
+	// -----------------------------------
+
+	// Try to submit a guardian set update - should pass
+	initialIndex := int(helpers.QueryConsensusGuardianSetIndex(t, wormchain, ctx))
+	signingGuardians := guardians.CreateValSet(t, numVals)
+
+	newGuardians := signingGuardians
+	err = cw_wormhole.SubmitGuardianSetUpdate(t, ctx, wormchain, contractAddr, newGuardians, uint32(initialIndex+1), oldGuardians)
+	require.NoError(t, err)
+	cw_wormhole.VerifyGuardianSet(t, ctx, wormchain, contractAddr, newGuardians, initialIndex+1)
+
+	// -----------------------------------
+
+	// Migrate contract back to original contract id
+	err = cw_wormhole.SubmitContractUpgrade(t, ctx, signingGuardians, wormchain, contractAddr, contractInfo.ContractInfo.CodeID)
+	require.NoError(t, err)
+}
+
+// TestShutdownTokenBridge tests the endpoints of a contract in its "shutdown" state
+// The shutdown contract only allows the following: Upgrading the Contract & Registering a new chain.
+func TestShutdownTokenBridge(t *testing.T) {
+	// Setup chain and contract
+	numVals := 1
+	guardians := guardians.CreateValSet(t, numVals)
+	chains := CreateChains(t, "v2.24.2", *guardians)
+	ctx, r, eRep, _ := BuildInterchain(t, chains)
+
+	// Chains
+	wormchain := chains[0].(*cosmos.CosmosChain)
+	gaia := chains[1].(*cosmos.CosmosChain)
+	osmosis := chains[2].(*cosmos.CosmosChain)
+
+	wormchainFaucetAddrBz, err := wormchain.GetAddress(ctx, "faucet")
+	require.NoError(t, err)
+	wormchainFaucetAddr := sdk.MustBech32ifyAddressBytes(wormchain.Config().Bech32Prefix, wormchainFaucetAddrBz)
+	fmt.Println("Wormchain faucet addr: ", wormchainFaucetAddr)
+
+	osmoToWormChannel, err := ibc.GetTransferChannel(ctx, r, eRep, osmosis.Config().ChainID, wormchain.Config().ChainID)
+	require.NoError(t, err)
+	wormToOsmoChannel := osmoToWormChannel.Counterparty
+	gaiaToWormChannel, err := ibc.GetTransferChannel(ctx, r, eRep, gaia.Config().ChainID, wormchain.Config().ChainID)
+	require.NoError(t, err)
+	wormToGaiaChannel := gaiaToWormChannel.Counterparty
+
+	users := interchaintest.GetAndFundTestUsers(t, ctx, "default", int64(10_000_000_000), gaia, osmosis, osmosis)
+	gaiaUser := users[0]
+	osmoUser1 := users[1]
+	osmoUser2 := users[2]
+
+	// Store wormhole core contract
+	coreContractCodeId := helpers.StoreContract(t, ctx, wormchain, "faucet", "./contracts/wormhole_core.wasm", guardians)
+	fmt.Println("Core contract code id: ", coreContractCodeId)
+
+	// Instantiate wormhole core contract
+	coreInstantiateMsg := helpers.CoreContractInstantiateMsg(t, wormchainConfig, vaa.ChainIDWormchain, guardians)
+	coreContractAddr := helpers.InstantiateContract(t, ctx, wormchain, "faucet", coreContractCodeId, "wormhole_core", coreInstantiateMsg, guardians)
+	fmt.Println("Core contract address: ", coreContractAddr)
+
+	// Store cw20_wrapped_2 contract
+	wrappedAssetCodeId := helpers.StoreContract(t, ctx, wormchain, "faucet", "./contracts/cw20_wrapped_2.wasm", guardians)
+	fmt.Println("CW20 wrapped_2 code id: ", wrappedAssetCodeId)
+
+	// Store token bridge contract
+	tbContractCodeId := helpers.StoreContract(t, ctx, wormchain, "faucet", "./contracts/token_bridge.wasm", guardians)
+	fmt.Println("Token bridge contract code id: ", tbContractCodeId)
+
+	// Instantiate token bridge contract
+	tbInstantiateMsg := helpers.TbContractInstantiateMsg(t, wormchainConfig, coreContractAddr, wrappedAssetCodeId)
+	tbContractAddr := helpers.InstantiateContract(t, ctx, wormchain, "faucet", tbContractCodeId, "token_bridge", tbInstantiateMsg, guardians)
+	fmt.Println("Token bridge contract address: ", tbContractAddr)
+
+	helpers.SubmitAllowlistInstantiateContract(t, ctx, wormchain, "faucet", wormchain.Config(), tbContractAddr, wrappedAssetCodeId, guardians)
+
+	// Store a new version of the token bridge to upgrade to
+	tbNewCodeId := helpers.StoreContract(t, ctx, wormchain, "faucet", "./contracts/shutdown_token_bridge.wasm", guardians)
+
+	// Submit contract upgrade
+	err = cw_wormhole.SubmitContractUpgrade(t, ctx, guardians, wormchain, tbContractAddr, tbNewCodeId)
+	require.NoError(t, err)
+
+	// -----------------------------------
+
+	// Registering a new chain - should pass
+	tbRegisterChainMsg := helpers.TbRegisterChainMsg(t, ExternalChainId, ExternalChainEmitterAddr, guardians)
+	_, err = wormchain.ExecuteContract(ctx, "faucet", tbContractAddr, string(tbRegisterChainMsg))
+	require.NoError(t, err)
+
+	// -----------------------------------
+
+	// Registering a new token - should fail
+	tbRegisterForeignAssetMsg := helpers.TbRegisterForeignAsset(t, Asset1ContractAddr, Asset1ChainID, ExternalChainEmitterAddr, Asset1Decimals, Asset1Symbol, Asset1Name, guardians)
+	_, err = wormchain.ExecuteContract(ctx, "faucet", tbContractAddr, string(tbRegisterForeignAssetMsg))
+	require.Error(t, err)
+	require.ErrorContains(t, err, "InvalidVAAAction")
+
+	// -----------------------------------
+
+	// Upgrade contract back to original contract id - should pass
+	err = cw_wormhole.SubmitContractUpgrade(t, ctx, guardians, wormchain, tbContractAddr, tbContractCodeId)
+	require.NoError(t, err)
+
+	// -----------------------------------
+
+	// Setup chains in "full" mode in preparation for transfer vaas
+
+	// Now register a new token - should pass on original contract
+	tbRegisterForeignAssetMsg = helpers.TbRegisterForeignAsset(t, Asset1ContractAddr, Asset1ChainID, ExternalChainEmitterAddr, Asset1Decimals, Asset1Symbol, Asset1Name, guardians)
+	_, err = wormchain.ExecuteContract(ctx, "faucet", tbContractAddr, string(tbRegisterForeignAssetMsg))
+	require.NoError(t, err)
+
+	// Store ibc translator contract
+	ibcTranslatorCodeId := helpers.StoreContract(t, ctx, wormchain, "faucet", "./contracts/ibc_translator.wasm", guardians)
+	fmt.Println("Ibc translator code id: ", ibcTranslatorCodeId)
+
+	// Instantiate ibc translator contract
+	ibcTranslatorInstantiateMsg := helpers.IbcTranslatorContractInstantiateMsg(t, tbContractAddr)
+	ibcTranslatorContractAddr := helpers.InstantiateContract(t, ctx, wormchain, "faucet", ibcTranslatorCodeId, "ibc_translator", ibcTranslatorInstantiateMsg, guardians)
+	fmt.Println("Ibc translator contract address: ", ibcTranslatorContractAddr)
+
+	helpers.SetMiddlewareContract(t, ctx, wormchain, "faucet", wormchain.Config(), ibcTranslatorContractAddr, guardians)
+
+	// Allowlist worm/osmo chain id / channel
+	wormOsmoAllowlistMsg := helpers.SubmitUpdateChainToChannelMapMsg(t, OsmoChainID, wormToOsmoChannel.ChannelID, guardians)
+	_, err = wormchain.ExecuteContract(ctx, "faucet", ibcTranslatorContractAddr, wormOsmoAllowlistMsg)
+	require.NoError(t, err)
+
+	// Allowlist worm/gaia chain id / channel
+	wormGaiaAllowlistMsg := helpers.SubmitUpdateChainToChannelMapMsg(t, GaiaChainID, wormToGaiaChannel.ChannelID, guardians)
+	_, err = wormchain.ExecuteContract(ctx, "faucet", ibcTranslatorContractAddr, wormGaiaAllowlistMsg)
+	require.NoError(t, err)
+
+	ibcHooksCodeId, err := osmosis.StoreContract(ctx, osmoUser1.KeyName, "./contracts/ibc_hooks.wasm")
+	require.NoError(t, err)
+	fmt.Println("IBC hooks code id: ", ibcHooksCodeId)
+
+	ibcHooksContractAddr, err := osmosis.InstantiateContract(ctx, osmoUser1.KeyName, ibcHooksCodeId, "{}", true)
+	require.NoError(t, err)
+	fmt.Println("IBC hooks contract addr: ", ibcHooksContractAddr)
+
+	// -----------------------------------
+
+	// Upgrade contract to shutdown contract
+	err = cw_wormhole.SubmitContractUpgrade(t, ctx, guardians, wormchain, tbContractAddr, tbNewCodeId)
+	require.NoError(t, err)
+
+	// -----------------------------------
+
+	// Send transfer vaas - all should fail
+
+	// Create and process a simple ibc payload3: Transfers 10.000_018 of asset1 from external chain through wormchain to gaia user
+	simplePayload := helpers.CreateGatewayIbcTokenBridgePayloadTransfer(t, GaiaChainID, gaiaUser.Bech32Address(gaia.Config().Bech32Prefix), 0, 1)
+	externalSender := []byte{1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8}
+	payload3 := helpers.CreatePayload3(wormchain.Config(), uint64(AmountExternalToGaiaUser1), Asset1ContractAddr, Asset1ChainID, ibcTranslatorContractAddr, uint16(vaa.ChainIDWormchain), externalSender, simplePayload)
+	completeTransferAndConvertMsg := helpers.IbcTranslatorCompleteTransferAndConvertMsg(t, ExternalChainId, ExternalChainEmitterAddr, payload3, guardians)
+	_, err = wormchain.ExecuteContract(ctx, "faucet", ibcTranslatorContractAddr, completeTransferAndConvertMsg)
+	require.Error(t, err)
+	require.ErrorContains(t, err, "Invalid during shutdown mode")
+
+	// Create and process a simple ibc payload3: Transfers 1.000_001 of asset1 from external chain through wormchain to osmo user1
+	simplePayload = helpers.CreateGatewayIbcTokenBridgePayloadTransfer(t, OsmoChainID, osmoUser1.Bech32Address(osmosis.Config().Bech32Prefix), 0, 1)
+	payload3 = helpers.CreatePayload3(wormchain.Config(), uint64(AmountExternalToOsmoUser1), Asset1ContractAddr, Asset1ChainID, ibcTranslatorContractAddr, uint16(vaa.ChainIDWormchain), externalSender, simplePayload)
+	completeTransferAndConvertMsg = helpers.IbcTranslatorCompleteTransferAndConvertMsg(t, ExternalChainId, ExternalChainEmitterAddr, payload3, guardians)
+	_, err = wormchain.ExecuteContract(ctx, "faucet", ibcTranslatorContractAddr, completeTransferAndConvertMsg)
+	require.Error(t, err)
+	require.ErrorContains(t, err, "Invalid during shutdown mode")
+
+	// Create and process a contract controlled ibc payload3
+	// Transfers 1.000_002 of asset1 from external chain through wormchain to ibc hooks contract addr
+	// IBC hooks is used to route the contract controlled payload to a test contract which forwards tokens to osmo user2
+	ibcHooksPayload := helpers.CreateIbcHooksMsg(t, ibcHooksContractAddr, osmoUser2.Bech32Address(osmosis.Config().Bech32Prefix))
+	contractControlledPayload := helpers.CreateGatewayIbcTokenBridgePayloadTransferWithPayload(t, OsmoChainID, ibcHooksContractAddr, ibcHooksPayload, 1)
+	payload3 = helpers.CreatePayload3(wormchain.Config(), uint64(AmountExternalToOsmoUser2), Asset1ContractAddr, Asset1ChainID, ibcTranslatorContractAddr, uint16(vaa.ChainIDWormchain), externalSender, contractControlledPayload)
+	completeTransferAndConvertMsg = helpers.IbcTranslatorCompleteTransferAndConvertMsg(t, ExternalChainId, ExternalChainEmitterAddr, payload3, guardians)
+	_, err = wormchain.ExecuteContract(ctx, "faucet", ibcTranslatorContractAddr, completeTransferAndConvertMsg)
+	require.Error(t, err)
+	require.ErrorContains(t, err, "Invalid during shutdown mode")
+}