浏览代码

cosmwasm: ibc contracts (#2591)

* cosmwasm: add wormchain-ibc-receiver and wormhole-ibc contracts

* Address review comments from jynnantonix and hendrikhofstadt

* Fix lint errors and test failures

* Update naming to reflect new mapping of channelId -> chainId

* Return errors in ibc handlers that should never be called

* Remove contract name and version logic from migration handlers

* Add query handlers to wormhole-ibc contract

* Add wormchain channel id whitelisting to wormhole-ibc contract

* Increase packet timeout to 1 year

* Rebase on main, update imports to new names

* Add governance replay protection to both contracts

* wormhole_ibc SubmitUpdateChannelChain should only handle a single VAA

* better error messages

* Better logging and strip null characters from channel_id from governance VAA

* add brackets back for empty query methods

* Update Cargo.lock

* Only send wormhole wasm event attributes via IBC and add attribute whitelist on the receiver end

* tilt: fix terra2 deploy

* Update based on comments from jynnantonix

---------

Co-authored-by: Evan Gray <battledingo@gmail.com>
Nikhil Suri 2 年之前
父节点
当前提交
892274ffa4

文件差异内容过多而无法显示
+ 241 - 166
cosmwasm/Cargo.lock


+ 4 - 0
cosmwasm/Cargo.toml

@@ -10,6 +10,8 @@ members = [
     "contracts/global-accountant",
     "packages/wormhole-bindings",
     "packages/cw_transcode",
+    "contracts/wormhole-ibc",
+    "contracts/wormchain-ibc-receiver"
 ]
 
 # Needed to prevent unwanted feature unification between normal builds and dev builds.  See
@@ -37,3 +39,5 @@ global-accountant = { path = "contracts/global-accountant" }
 wormhole-bindings = { path = "packages/wormhole-bindings" }
 wormhole-cosmwasm = { path = "contracts/wormhole" }
 wormhole-sdk = { path = "../sdk/rust/core" }
+wormchain-ibc-receiver = { path = "contracts/wormchain-ibc-receiver" }
+wormhole-ibc = { path = "contracts/wormhole-ibc" }

+ 5 - 0
cosmwasm/contracts/wormchain-ibc-receiver/.cargo/config

@@ -0,0 +1,5 @@
+[alias]
+wasm = "build --release --target wasm32-unknown-unknown"
+wasm-debug = "build --target wasm32-unknown-unknown"
+unit-test = "test --lib --features backtraces"
+integration-test = "test --test integration"

+ 22 - 0
cosmwasm/contracts/wormchain-ibc-receiver/Cargo.toml

@@ -0,0 +1,22 @@
+[package]
+name = "wormchain-ibc-receiver"
+version = "0.1.0"
+authors = ["Wormhole Project Contributors"]
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[features]
+backtraces = ["cosmwasm-std/backtraces"]
+
+[dependencies]
+cosmwasm-std = { version = "1.0.0", features = ["ibc3"] }
+cosmwasm-schema = "1"
+cw-storage-plus = "0.13.2"
+anyhow = "1"
+semver = "1.0.16"
+thiserror = "1.0.31"
+wormhole-bindings = "0.1.0"
+wormhole-sdk = { version = "0.1.0", features = ["schemars"] }
+serde_wormhole = "0.1.0"

+ 157 - 0
cosmwasm/contracts/wormchain-ibc-receiver/src/contract.rs

@@ -0,0 +1,157 @@
+use crate::error::ContractError;
+use crate::msg::{AllChannelChainsResponse, ChannelChainResponse, ExecuteMsg, QueryMsg};
+use crate::state::{CHANNEL_CHAIN, VAA_ARCHIVE};
+use anyhow::{bail, ensure, Context};
+use cosmwasm_std::{entry_point, to_binary, Binary, Deps, Empty, Event, StdResult};
+use cosmwasm_std::{DepsMut, Env, MessageInfo, Order, Response};
+use serde_wormhole::RawMessage;
+use std::str;
+use wormhole_bindings::WormholeQuery;
+use wormhole_sdk::ibc_receiver::{Action, GovernancePacket};
+use wormhole_sdk::vaa::{Body, Header};
+use wormhole_sdk::Chain;
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn instantiate(
+    _deps: DepsMut,
+    _env: Env,
+    info: MessageInfo,
+    _msg: Empty,
+) -> Result<Response, anyhow::Error> {
+    Ok(Response::new()
+        .add_attribute("action", "instantiate")
+        .add_attribute("owner", info.sender))
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn migrate(_deps: DepsMut, _env: Env, _msg: Empty) -> Result<Response, anyhow::Error> {
+    Ok(Response::default())
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn execute(
+    deps: DepsMut<WormholeQuery>,
+    _env: Env,
+    info: MessageInfo,
+    msg: ExecuteMsg,
+) -> Result<Response, anyhow::Error> {
+    match msg {
+        ExecuteMsg::SubmitUpdateChannelChain { vaas } => submit_vaas(deps, info, vaas),
+    }
+}
+
+fn submit_vaas(
+    mut deps: DepsMut<WormholeQuery>,
+    info: MessageInfo,
+    vaas: Vec<Binary>,
+) -> Result<Response, anyhow::Error> {
+    let evts = vaas
+        .into_iter()
+        .map(|v| handle_vaa(deps.branch(), v))
+        .collect::<anyhow::Result<Vec<_>>>()?;
+    Ok(Response::new()
+        .add_attribute("action", "submit_vaas")
+        .add_attribute("owner", info.sender)
+        .add_events(evts))
+}
+
+fn handle_vaa(deps: DepsMut<WormholeQuery>, vaa: Binary) -> anyhow::Result<Event> {
+    // parse the VAA header and data
+    let (header, data) = serde_wormhole::from_slice::<(Header, &RawMessage)>(&vaa)
+        .context("failed to parse VAA header")?;
+
+    // Must be a version 1 VAA
+    ensure!(header.version == 1, "unsupported VAA version");
+
+    // call into wormchain to verify the VAA
+    deps.querier
+        .query::<Empty>(&WormholeQuery::VerifyVaa { vaa: vaa.clone() }.into())
+        .context(ContractError::VerifyQuorum)?;
+
+    // parse the VAA body
+    let body = serde_wormhole::from_slice::<Body<&RawMessage>>(data)
+        .context("failed to parse VAA body")?;
+
+    // validate this is a governance VAA
+    ensure!(
+        body.emitter_chain == Chain::Solana
+            && body.emitter_address == wormhole_sdk::GOVERNANCE_EMITTER,
+        "not a governance VAA"
+    );
+
+    // parse the governance packet
+    let govpacket: GovernancePacket =
+        serde_wormhole::from_slice(body.payload).context("failed to parse governance packet")?;
+
+    // validate the governance VAA is directed to wormchain
+    ensure!(
+        govpacket.chain == Chain::Wormchain,
+        "this governance VAA is for another chain"
+    );
+
+    // governance VAA replay protection
+    let digest = body
+        .digest()
+        .context("failed to compute governance VAA digest")?;
+
+    if VAA_ARCHIVE.has(deps.storage, &digest.hash) {
+        bail!("governance vaa already executed");
+    }
+    VAA_ARCHIVE
+        .save(deps.storage, &digest.hash, &true)
+        .context("failed to save governance VAA to archive")?;
+
+    // match the governance action and execute the corresponding logic
+    match govpacket.action {
+        Action::UpdateChannelChain {
+            channel_id,
+            chain_id,
+        } => {
+            ensure!(chain_id != Chain::Wormchain, "the wormchain-ibc-receiver contract should not maintain channel mappings to wormchain");
+
+            let channel_id_str =
+                str::from_utf8(&channel_id).context("failed to parse channel-id as utf-8")?;
+            let channel_id_trimmed = channel_id_str.trim_start_matches(char::from(0));
+
+            // update storage with the mapping
+            CHANNEL_CHAIN
+                .save(
+                    deps.storage,
+                    channel_id_trimmed.to_string(),
+                    &chain_id.into(),
+                )
+                .context("failed to save channel chain")?;
+            Ok(Event::new("UpdateChannelChain")
+                .add_attribute("chain_id", chain_id.to_string())
+                .add_attribute("channel_id", channel_id_trimmed))
+        }
+    }
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
+    match msg {
+        QueryMsg::ChannelChain { channel_id } => {
+            query_channel_chain(deps, channel_id).and_then(|resp| to_binary(&resp))
+        }
+        QueryMsg::AllChannelChains {} => {
+            query_all_channel_chains(deps).and_then(|resp| to_binary(&resp))
+        }
+    }
+}
+
+fn query_channel_chain(deps: Deps, channel_id: Binary) -> StdResult<ChannelChainResponse> {
+    CHANNEL_CHAIN
+        .load(deps.storage, channel_id.to_string())
+        .map(|chain_id| ChannelChainResponse { chain_id })
+}
+
+fn query_all_channel_chains(deps: Deps) -> StdResult<AllChannelChainsResponse> {
+    CHANNEL_CHAIN
+        .range(deps.storage, None, None, Order::Ascending)
+        .map(|res| {
+            res.map(|(channel_id, chain_id)| (Binary::from(Vec::<u8>::from(channel_id)), chain_id))
+        })
+        .collect::<StdResult<Vec<_>>>()
+        .map(|channels_chains| AllChannelChainsResponse { channels_chains })
+}

+ 7 - 0
cosmwasm/contracts/wormchain-ibc-receiver/src/error.rs

@@ -0,0 +1,7 @@
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum ContractError {
+    #[error("failed to verify quorum")]
+    VerifyQuorum,
+}

+ 180 - 0
cosmwasm/contracts/wormchain-ibc-receiver/src/ibc.rs

@@ -0,0 +1,180 @@
+use anyhow::{bail, ensure};
+use cosmwasm_std::{
+    entry_point, from_slice, to_binary, Attribute, Binary, ContractResult, DepsMut, Env,
+    Ibc3ChannelOpenResponse, IbcBasicResponse, IbcChannelCloseMsg, IbcChannelConnectMsg,
+    IbcChannelOpenMsg, IbcChannelOpenResponse, IbcPacketAckMsg, IbcPacketReceiveMsg,
+    IbcPacketTimeoutMsg, IbcReceiveResponse, StdError, StdResult,
+};
+
+use crate::msg::WormholeIbcPacketMsg;
+
+// Implementation of IBC protocol
+// Implements 6 entry points that are required for the x/wasm runtime to bind a port for this contract
+// https://github.com/CosmWasm/cosmwasm/blob/main/IBC.md#writing-new-protocols
+
+pub const IBC_APP_VERSION: &str = "ibc-wormhole-v1";
+
+/// 1. Opening a channel. Step 1 of handshake. Combines ChanOpenInit and ChanOpenTry from the spec.
+/// The only valid action of the contract is to accept the channel or reject it.
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn ibc_channel_open(
+    _deps: DepsMut,
+    _env: Env,
+    msg: IbcChannelOpenMsg,
+) -> StdResult<IbcChannelOpenResponse> {
+    let channel = msg.channel();
+
+    if channel.version.as_str() != IBC_APP_VERSION {
+        return Err(StdError::generic_err(format!(
+            "Must set version to `{}`",
+            IBC_APP_VERSION
+        )));
+    }
+
+    if let Some(counter_version) = msg.counterparty_version() {
+        if counter_version != IBC_APP_VERSION {
+            return Err(StdError::generic_err(format!(
+                "Counterparty version must be `{}`",
+                IBC_APP_VERSION
+            )));
+        }
+    }
+
+    // We return the version we need (which could be different than the counterparty version)
+    Ok(Some(Ibc3ChannelOpenResponse {
+        version: IBC_APP_VERSION.to_string(),
+    }))
+}
+
+/// 2. Step 2 of handshake. Combines ChanOpenAck and ChanOpenConfirm from the spec.
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn ibc_channel_connect(
+    _deps: DepsMut,
+    _env: Env,
+    msg: IbcChannelConnectMsg,
+) -> StdResult<IbcBasicResponse> {
+    let channel = msg.channel();
+    let connection_id = &channel.connection_id;
+
+    Ok(IbcBasicResponse::new()
+        .add_attribute("action", "ibc_connect")
+        .add_attribute("connection_id", connection_id))
+}
+
+/// 3. Closing a channel - whether due to an IBC error, at our request, or at the request of the other side.
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn ibc_channel_close(
+    _deps: DepsMut,
+    _env: Env,
+    _msg: IbcChannelCloseMsg,
+) -> StdResult<IbcBasicResponse> {
+    Err(StdError::generic_err("user cannot close channel"))
+}
+
+/// 4. Receiving a packet.
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn ibc_packet_receive(
+    _deps: DepsMut,
+    _env: Env,
+    msg: IbcPacketReceiveMsg,
+) -> StdResult<IbcReceiveResponse> {
+    handle_packet_receive(msg).or_else(|e| {
+        // we try to capture all app-level errors and convert them into
+        // acknowledgement packets that contain an error code.
+        let acknowledgement = encode_ibc_error(format!("invalid packet: {}", e));
+        Ok(IbcReceiveResponse::new()
+            .set_ack(acknowledgement)
+            .add_attribute("action", "ibc_packet_ack"))
+    })
+}
+
+/// Decode the IBC packet as WormholeIbcPacketMsg::Publish and take appropriate action
+fn handle_packet_receive(msg: IbcPacketReceiveMsg) -> Result<IbcReceiveResponse, anyhow::Error> {
+    let packet = msg.packet;
+    // which local channel did this packet come on
+    let channel_id = packet.dest.channel_id;
+    let wormhole_msg: WormholeIbcPacketMsg = from_slice(&packet.data)?;
+    match wormhole_msg {
+        WormholeIbcPacketMsg::Publish { msg: publish_attrs } => {
+            receive_publish(channel_id, publish_attrs)
+        }
+    }
+}
+
+const EXPECTED_WORMHOLE_IBC_EVENT_ATTRS: [&str; 8] = [
+    "message.message",
+    "message.sender",
+    "message.chain_id",
+    "message.nonce",
+    "message.sequence",
+    "message.block_time",
+    "message.tx_index",
+    "message.block_height",
+];
+
+fn receive_publish(
+    channel_id: String,
+    publish_attrs: Vec<Attribute>,
+) -> Result<IbcReceiveResponse, anyhow::Error> {
+    // check the attributes are what we expect from wormhole
+    ensure!(
+        publish_attrs.len() == EXPECTED_WORMHOLE_IBC_EVENT_ATTRS.len(),
+        "number of received attributes does not match number of expected"
+    );
+
+    for key in EXPECTED_WORMHOLE_IBC_EVENT_ATTRS {
+        let mut matched = false;
+        for attr in &publish_attrs {
+            if key == attr.key {
+                matched = true;
+                break;
+            }
+        }
+        if !matched {
+            bail!(
+                "expected attribute unmmatched in received attributes: {}",
+                key
+            );
+        }
+    }
+
+    // send the ack and emit the message with the attributes from the wormhole message
+    let acknowledgement = to_binary(&ContractResult::<()>::Ok(()))?;
+    Ok(IbcReceiveResponse::new()
+        .set_ack(acknowledgement)
+        .add_attribute("action", "receive_publish")
+        .add_attribute("channel_id", channel_id)
+        .add_attributes(publish_attrs))
+}
+
+// this encode an error or error message into a proper acknowledgement to the recevier
+fn encode_ibc_error(msg: impl Into<String>) -> Binary {
+    // this cannot error, unwrap to keep the interface simple
+    to_binary(&ContractResult::<()>::Err(msg.into())).unwrap()
+}
+
+/// 5. Acknowledging a packet. Called when the other chain successfully receives a packet from us.
+/// Never should be called as this contract never sends packets
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn ibc_packet_ack(
+    _deps: DepsMut,
+    _env: Env,
+    _msg: IbcPacketAckMsg,
+) -> StdResult<IbcBasicResponse> {
+    Err(StdError::generic_err(
+        "ack should never be called as this contract never sends packets",
+    ))
+}
+
+/// 6. Timing out a packet. Called when the packet was not recieved on the other chain before the timeout.
+/// Never should be called as this contract never sends packets
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn ibc_packet_timeout(
+    _deps: DepsMut,
+    _env: Env,
+    _msg: IbcPacketTimeoutMsg,
+) -> StdResult<IbcBasicResponse> {
+    Err(StdError::generic_err(
+        "timeout should never be called as this contract never sends packets",
+    ))
+}

+ 5 - 0
cosmwasm/contracts/wormchain-ibc-receiver/src/lib.rs

@@ -0,0 +1,5 @@
+pub mod contract;
+pub mod error;
+pub mod ibc;
+pub mod msg;
+pub mod state;

+ 40 - 0
cosmwasm/contracts/wormchain-ibc-receiver/src/msg.rs

@@ -0,0 +1,40 @@
+use cosmwasm_schema::{cw_serde, QueryResponses};
+use cosmwasm_std::{Attribute, Binary};
+
+#[cw_serde]
+pub enum ExecuteMsg {
+    /// Submit one or more signed VAAs to update the on-chain state.  If processing any of the VAAs
+    /// returns an error, the entire transaction is aborted and none of the VAAs are committed.
+    SubmitUpdateChannelChain {
+        /// One or more VAAs to be submitted.  Each VAA should be encoded in the standard wormhole
+        /// wire format.
+        vaas: Vec<Binary>,
+    },
+}
+
+/// This is the message we send over the IBC channel
+#[cw_serde]
+pub enum WormholeIbcPacketMsg {
+    Publish { msg: Vec<Attribute> },
+}
+
+/// Contract queries
+#[cw_serde]
+#[derive(QueryResponses)]
+pub enum QueryMsg {
+    #[returns(AllChannelChainsResponse)]
+    AllChannelChains {},
+    #[returns(ChannelChainResponse)]
+    ChannelChain { channel_id: Binary },
+}
+
+#[cw_serde]
+pub struct AllChannelChainsResponse {
+    // a tuple of (channelId, chainId)
+    pub channels_chains: Vec<(Binary, u16)>,
+}
+
+#[cw_serde]
+pub struct ChannelChainResponse {
+    pub chain_id: u16,
+}

+ 4 - 0
cosmwasm/contracts/wormchain-ibc-receiver/src/state.rs

@@ -0,0 +1,4 @@
+use cw_storage_plus::Map;
+
+pub const CHANNEL_CHAIN: Map<String, u16> = Map::new("channel_chain");
+pub const VAA_ARCHIVE: Map<&[u8], bool> = Map::new("vaa_archive");

+ 5 - 0
cosmwasm/contracts/wormhole-ibc/.cargo/config

@@ -0,0 +1,5 @@
+[alias]
+wasm = "build --release --target wasm32-unknown-unknown"
+wasm-debug = "build --target wasm32-unknown-unknown"
+unit-test = "test --lib --features backtraces"
+integration-test = "test --test integration"

+ 27 - 0
cosmwasm/contracts/wormhole-ibc/Cargo.toml

@@ -0,0 +1,27 @@
+[package]
+name = "wormhole-ibc"
+version = "0.1.0"
+authors = ["Wormhole Project Contributors"]
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[features]
+backtraces = ["cosmwasm-std/backtraces"]
+
+[dependencies]
+wormhole-cosmwasm = { version = "0.1.0", default-features = false, features = ["library", "full"] }
+cosmwasm-std = { version = "1.0.0", features = ["ibc3"] }
+cw-storage-plus = "0.13.2"
+cosmwasm-schema = "1"
+anyhow = "1"
+schemars = "0.8.8"
+serde = { version = "1.0.137", default-features = false, features = ["derive"] }
+semver = "1.0.16"
+thiserror = "1.0.31"
+serde_wormhole = "0.1.0"
+wormhole-sdk = { version = "0.1.0", features = ["schemars"] }
+
+[dev-dependencies]
+hex = "0.4.3"

+ 188 - 0
cosmwasm/contracts/wormhole-ibc/src/contract.rs

@@ -0,0 +1,188 @@
+#[cfg(not(feature = "library"))]
+use cosmwasm_std::entry_point;
+use cw_wormhole::{
+    contract::{
+        execute as core_execute, instantiate as core_instantiate, migrate as core_migrate,
+        query as core_query, query_parse_and_verify_vaa,
+    },
+    state::config_read,
+};
+use wormhole_sdk::{
+    ibc_receiver::{Action, GovernancePacket},
+    Chain,
+};
+
+use crate::{
+    ibc::PACKET_LIFETIME,
+    msg::ExecuteMsg,
+    state::{VAA_ARCHIVE, WORMCHAIN_CHANNEL_ID},
+};
+use anyhow::{bail, ensure, Context};
+use cosmwasm_std::{
+    to_binary, Binary, Deps, DepsMut, Env, Event, IbcMsg, MessageInfo, Response, StdResult,
+};
+use cw_wormhole::msg::{ExecuteMsg as WormholeExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
+
+use crate::msg::WormholeIbcPacketMsg;
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn instantiate(
+    deps: DepsMut,
+    env: Env,
+    info: MessageInfo,
+    msg: InstantiateMsg,
+) -> Result<Response, anyhow::Error> {
+    // execute the wormhole core contract instantiation
+    core_instantiate(deps, env, info, msg).context("wormhole core instantiation failed")
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result<Response, anyhow::Error> {
+    // call the core contract migrate function
+    core_migrate(deps, env, msg).context("wormhole core migration failed")
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn execute(
+    deps: DepsMut,
+    env: Env,
+    info: MessageInfo,
+    msg: ExecuteMsg,
+) -> Result<Response, anyhow::Error> {
+    match msg {
+        ExecuteMsg::SubmitVAA { vaa } => {
+            core_execute(deps, env, info, WormholeExecuteMsg::SubmitVAA { vaa })
+                .context("failed core submit_vaa execution")
+        }
+        ExecuteMsg::PostMessage { message, nonce } => post_message_ibc(
+            deps,
+            env,
+            info,
+            WormholeExecuteMsg::PostMessage { message, nonce },
+        ),
+        ExecuteMsg::SubmitUpdateChannelChain { vaa } => {
+            let evt = handle_vaa(deps, env, vaa)?;
+            Ok(Response::new()
+                .add_attribute("action", "submit_vaas")
+                .add_attribute("owner", info.sender)
+                .add_event(evt))
+        }
+    }
+}
+
+fn handle_vaa(deps: DepsMut, env: Env, vaa: Binary) -> anyhow::Result<Event> {
+    // parse the VAA header and data
+    let vaa = query_parse_and_verify_vaa(deps.as_ref(), vaa.as_slice(), env.block.time.seconds())
+        .context("failed to parse vaa")?;
+
+    // validate this is a governance VAA
+    ensure!(
+        Chain::from(vaa.emitter_chain) == Chain::Solana
+            && vaa.emitter_address == wormhole_sdk::GOVERNANCE_EMITTER.0,
+        "not a governance VAA"
+    );
+
+    // parse the governance packet
+    let govpacket = serde_wormhole::from_slice::<GovernancePacket>(&vaa.payload)
+        .context("failed to parse governance packet")?;
+
+    // validate the governance VAA is directed to this chain
+    let state = config_read(deps.storage)
+        .load()
+        .context("failed to load contract config")?;
+    ensure!(
+        govpacket.chain == Chain::from(state.chain_id),
+        format!(
+            "this governance VAA is for chain {}, which does not match this chain ({})",
+            u16::from(govpacket.chain),
+            state.chain_id
+        )
+    );
+
+    // governance VAA replay protection
+    if VAA_ARCHIVE.has(deps.storage, vaa.hash.as_slice()) {
+        bail!("governance vaa already executed");
+    }
+    VAA_ARCHIVE
+        .save(deps.storage, vaa.hash.as_slice(), &true)
+        .context("failed to save governance VAA to archive")?;
+
+    // match the governance action and execute the corresponding logic
+    match govpacket.action {
+        Action::UpdateChannelChain {
+            channel_id,
+            chain_id,
+        } => {
+            // validate that the chain_id for the channel is wormchain
+            // we should only be whitelisting IBC connections to wormchain
+            ensure!(
+                chain_id == Chain::Wormchain,
+                "whitelisted ibc channel not for wormchain"
+            );
+
+            let channel_id_str = String::from_utf8(channel_id.to_vec())
+                .context("failed to parse channel-id as utf-8")?;
+            let channel_id_trimmed = channel_id_str.trim_start_matches(char::from(0));
+
+            // update the whitelisted wormchain channel id
+            WORMCHAIN_CHANNEL_ID
+                .save(deps.storage, &channel_id_trimmed.to_string())
+                .context("failed to save channel chain")?;
+            Ok(Event::new("UpdateChannelChain")
+                .add_attribute("chain_id", chain_id.to_string())
+                .add_attribute("channel_id", channel_id_trimmed))
+        }
+    }
+}
+
+fn post_message_ibc(
+    deps: DepsMut,
+    env: Env,
+    info: MessageInfo,
+    msg: WormholeExecuteMsg,
+) -> anyhow::Result<Response> {
+    let channel_id = WORMCHAIN_CHANNEL_ID
+        .load(deps.storage)
+        .context("failed to load whitelisted wormchain channel id")?;
+
+    // compute the packet timeout
+    let packet_timeout = env.block.time.plus_seconds(PACKET_LIFETIME).into();
+
+    // compute the block height
+    let block_height = env.block.height.to_string();
+
+    // compute the transaction index
+    // (this is an optional since not all messages are executed as part of txns)
+    // (they may be executed part of the pre/post block handlers)
+    let tx_index = env.transaction.as_ref().map(|tx_info| tx_info.index);
+
+    // actually execute the postMessage call on the core contract
+    let mut res = core_execute(deps, env, info, msg).context("wormhole core execution failed")?;
+
+    res = match tx_index {
+        Some(index) => res.add_attribute("message.tx_index", index.to_string()),
+        None => res,
+    };
+    res = res.add_attribute("message.block_height", block_height);
+
+    // Send the result attributes over IBC on this channel
+    let packet = WormholeIbcPacketMsg::Publish {
+        msg: res.attributes.clone(),
+    };
+    let ibc_msg = IbcMsg::SendPacket {
+        channel_id,
+        data: to_binary(&packet)?,
+        timeout: packet_timeout,
+    };
+
+    // add the IBC message to the response
+    Ok(res
+        .add_attribute("is_ibc", true.to_string())
+        .add_message(ibc_msg))
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
+    // defer to the core contract logic for all query handling
+    core_query(deps, env, msg)
+}

+ 106 - 0
cosmwasm/contracts/wormhole-ibc/src/ibc.rs

@@ -0,0 +1,106 @@
+use cosmwasm_std::{
+    entry_point, DepsMut, Env, Ibc3ChannelOpenResponse, IbcBasicResponse, IbcChannelCloseMsg,
+    IbcChannelConnectMsg, IbcChannelOpenMsg, IbcChannelOpenResponse, IbcPacketAckMsg,
+    IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, StdError, StdResult,
+};
+
+use std::str;
+
+// Implementation of IBC protocol
+// Implements 6 entry points that are required for the x/wasm runtime to bind a port for this contract
+// https://github.com/CosmWasm/cosmwasm/blob/main/IBC.md#writing-new-protocols
+
+pub const IBC_APP_VERSION: &str = "ibc-wormhole-v1";
+
+/// packets live one year
+pub const PACKET_LIFETIME: u64 = 31_536_000;
+
+/// 1. Opening a channel. Step 1 of handshake. Combines ChanOpenInit and ChanOpenTry from the spec.
+/// The only valid action of the contract is to accept the channel or reject it.
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn ibc_channel_open(
+    _deps: DepsMut,
+    _env: Env,
+    msg: IbcChannelOpenMsg,
+) -> StdResult<IbcChannelOpenResponse> {
+    let channel = msg.channel();
+
+    if channel.version.as_str() != IBC_APP_VERSION {
+        return Err(StdError::generic_err(format!(
+            "Must set version to `{}`",
+            IBC_APP_VERSION
+        )));
+    }
+
+    if let Some(counter_version) = msg.counterparty_version() {
+        if counter_version != IBC_APP_VERSION {
+            return Err(StdError::generic_err(format!(
+                "Counterparty version must be `{}`",
+                IBC_APP_VERSION
+            )));
+        }
+    }
+
+    // We return the version we need (which could be different than the counterparty version)
+    Ok(Some(Ibc3ChannelOpenResponse {
+        version: IBC_APP_VERSION.to_string(),
+    }))
+}
+
+/// 2. Step 2 of handshake. Combines ChanOpenAck and ChanOpenConfirm from the spec.
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn ibc_channel_connect(
+    _deps: DepsMut,
+    _env: Env,
+    msg: IbcChannelConnectMsg,
+) -> StdResult<IbcBasicResponse> {
+    let channel = msg.channel();
+    let channel_id = &channel.endpoint.channel_id;
+
+    Ok(IbcBasicResponse::new()
+        .add_attribute("action", "ibc_connect")
+        .add_attribute("channel_id", channel_id))
+}
+
+/// 3. Closing a channel - whether due to an IBC error, at our request, or at the request of the other side.
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn ibc_channel_close(
+    _deps: DepsMut,
+    _env: Env,
+    _msg: IbcChannelCloseMsg,
+) -> StdResult<IbcBasicResponse> {
+    Err(StdError::generic_err("user cannot close channel"))
+}
+
+/// 4. Receiving a packet.
+/// Never should be called as the other side never sends packets
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn ibc_packet_receive(
+    _deps: DepsMut,
+    _env: Env,
+    _msg: IbcPacketReceiveMsg,
+) -> StdResult<IbcReceiveResponse> {
+    Err(StdError::generic_err(
+        "receive should never be called as this contract should never receive packets",
+    ))
+}
+
+/// 5. Acknowledging a packet. Called when the other chain successfully receives a packet from us.
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn ibc_packet_ack(
+    _deps: DepsMut,
+    _env: Env,
+    _msg: IbcPacketAckMsg,
+) -> StdResult<IbcBasicResponse> {
+    Ok(IbcBasicResponse::new().add_attribute("action", "ibc_packet_ack"))
+}
+
+/// 6. Timing out a packet. Called when the packet was not recieved on the other chain before the timeout.
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn ibc_packet_timeout(
+    _deps: DepsMut,
+    _env: Env,
+    _msg: IbcPacketTimeoutMsg,
+) -> StdResult<IbcBasicResponse> {
+    Ok(IbcBasicResponse::new().add_attribute("action", "ibc_packet_timeout"))
+}

+ 4 - 0
cosmwasm/contracts/wormhole-ibc/src/lib.rs

@@ -0,0 +1,4 @@
+pub mod contract;
+pub mod ibc;
+pub mod msg;
+pub mod state;

+ 60 - 0
cosmwasm/contracts/wormhole-ibc/src/msg.rs

@@ -0,0 +1,60 @@
+use cosmwasm_schema::cw_serde;
+use cosmwasm_std::{Attribute, Binary};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+// TODO: figure out proper serde enum representation so we don't have to copy the core bridge execute message types
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ExecuteMsg {
+    SubmitVAA {
+        vaa: Binary,
+    },
+    PostMessage {
+        message: Binary,
+        nonce: u32,
+    },
+    /// Submit a signed VAA to update the on-chain state.  If processing any of the VAAs
+    /// returns an error, the entire transaction is aborted and none of the VAAs are committed.
+    SubmitUpdateChannelChain {
+        /// VAA to submit.  The VAA should be encoded in the standard wormhole
+        /// wire format.
+        vaa: Binary,
+    },
+}
+
+/// This is the message we send over the IBC channel
+#[cw_serde]
+pub enum WormholeIbcPacketMsg {
+    Publish { msg: Vec<Attribute> },
+}
+
+#[cfg(test)]
+mod test {
+    use cosmwasm_std::to_binary;
+    use cw_wormhole::msg::ExecuteMsg as WormholeExecuteMsg;
+
+    use super::ExecuteMsg;
+
+    #[test]
+    fn submit_vaa_serialization_matches() {
+        let signed_vaa = "\
+        080000000901007bfa71192f886ab6819fa4862e34b4d178962958d9b2e3d943\
+        7338c9e5fde1443b809d2886eaa69e0f0158ea517675d96243c9209c3fe1d94d\
+        5b19866654c6980000000b150000000500020001020304000000000000000000\
+        000000000000000000000000000000000000000000000000000a0261626364";
+        let signed_vaa = hex::decode(signed_vaa).unwrap();
+
+        let wormhole_submit_vaa = WormholeExecuteMsg::SubmitVAA {
+            vaa: signed_vaa.clone().into(),
+        };
+        let wormhole_msg = to_binary(&wormhole_submit_vaa).unwrap();
+
+        let submit_vaa = ExecuteMsg::SubmitVAA {
+            vaa: signed_vaa.into(),
+        };
+        let msg = to_binary(&submit_vaa).unwrap();
+
+        assert_eq!(wormhole_msg, msg);
+    }
+}

+ 4 - 0
cosmwasm/contracts/wormhole-ibc/src/state.rs

@@ -0,0 +1,4 @@
+use cw_storage_plus::{Item, Map};
+
+pub const WORMCHAIN_CHANNEL_ID: Item<String> = Item::new("wormchain_channel_id");
+pub const VAA_ARCHIVE: Map<&[u8], bool> = Map::new("vaa_archive");

+ 1 - 1
cosmwasm/contracts/wormhole/src/msg.rs

@@ -6,7 +6,7 @@ use crate::state::{GuardianAddress, GuardianSetInfo};
 
 type HumanAddr = String;
 
-/// The instantiation parameters of the token bridge contract. See
+/// The instantiation parameters of the core bridge contract. See
 /// [`crate::state::ConfigInfo`] for more details on what these fields mean.
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
 pub struct InstantiateMsg {

+ 2 - 0
cosmwasm/deployment/terra2/tools/deploy.js

@@ -23,6 +23,8 @@ const artifacts = [
   "shutdown_core_bridge_cosmwasm.wasm",
   "shutdown_token_bridge_cosmwasm.wasm",
   "global_accountant.wasm",
+  "wormchain_ibc_receiver.wasm",
+  "wormhole_ibc.wasm",
 ];
 
 /* Check that the artifact folder contains all the wasm files we expect and nothing else */

部分文件因为文件数量过多而无法显示