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>
This commit is contained in:
Nikhil Suri 2023-05-18 06:34:15 -05:00 committed by GitHub
parent 5aa99a959f
commit 892274ffa4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1435 additions and 263 deletions

876
cosmwasm/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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" }

View File

@ -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"

View File

@ -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"

View File

@ -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 })
}

View File

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

View File

@ -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",
))
}

View File

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

View File

@ -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,
}

View File

@ -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");

View File

@ -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"

View File

@ -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"

View File

@ -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)
}

View File

@ -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"))
}

View File

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

View File

@ -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);
}
}

View File

@ -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");

View File

@ -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 {

View File

@ -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 */