Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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>
- Loading branch information
Showing
19 changed files
with
1,428 additions
and
256 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 changes: 157 additions & 0 deletions
157
cosmwasm/contracts/wormchain-ibc-receiver/src/contract.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
use thiserror::Error; | ||
|
||
#[derive(Error, Debug)] | ||
pub enum ContractError { | ||
#[error("failed to verify quorum")] | ||
VerifyQuorum, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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", | ||
)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
pub mod contract; | ||
pub mod error; | ||
pub mod ibc; | ||
pub mod msg; | ||
pub mod state; |
Oops, something went wrong.