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:
parent
5aa99a959f
commit
892274ffa4
File diff suppressed because it is too large
Load Diff
|
@ -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" }
|
|
@ -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"
|
|
@ -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"
|
|
@ -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 })
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ContractError {
|
||||
#[error("failed to verify quorum")]
|
||||
VerifyQuorum,
|
||||
}
|
|
@ -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",
|
||||
))
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
pub mod contract;
|
||||
pub mod error;
|
||||
pub mod ibc;
|
||||
pub mod msg;
|
||||
pub mod state;
|
|
@ -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,
|
||||
}
|
|
@ -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");
|
|
@ -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"
|
|
@ -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"
|
|
@ -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)
|
||||
}
|
|
@ -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"))
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
pub mod contract;
|
||||
pub mod ibc;
|
||||
pub mod msg;
|
||||
pub mod state;
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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");
|
|
@ -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 {
|
||||
|
|
|
@ -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 */
|
||||
|
|
Loading…
Reference in New Issue