Add ibc-translator contract to the cosmwasm workspace and it's governance packet to the rust sdk. (#3174)

* Add ibc-translator contract and it's governance packet to the rust sdk.

* Remove unnecessary cargo script

* rustfmt and clippy fixes

* Address review comments

* Use WormholeQuery instead of contract to verify vaa, remove wormhole
core contract dependency, remove feature flag on anybuf.

* Add cw20 token metadata to tokenfactory metadata

* Change Simple/ContractControlled payloads to
GatewayTransfer/GatewayTransferWithPayload

* Change display and scaled denom to ensure x/bank conformance.
This commit is contained in:
Steve 2023-08-10 09:08:22 -05:00 committed by GitHub
parent 89e97fc366
commit 821a6e8752
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1372 additions and 51 deletions

152
cosmwasm/Cargo.lock generated
View File

@ -9,7 +9,7 @@ dependencies = [
"anyhow",
"cosmwasm-schema",
"cosmwasm-std",
"cw-storage-plus",
"cw-storage-plus 0.13.4",
"cw_transcode",
"hex",
"schemars",
@ -44,6 +44,12 @@ dependencies = [
"version_check",
]
[[package]]
name = "anybuf"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f14538dd5b6befdc12b11aae1dddee64b670534d4fdbd28057bcc7c976c0e4eb"
[[package]]
name = "anyhow"
version = "1.0.71"
@ -123,6 +129,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae"
[[package]]
name = "bs58"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3"
[[package]]
name = "bstr"
version = "1.4.0"
@ -214,9 +226,9 @@ dependencies = [
[[package]]
name = "cosmwasm-crypto"
version = "1.2.5"
version = "1.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75836a10cb9654c54e77ee56da94d592923092a10b369cdb0dbd56acefc16340"
checksum = "bb64554a91d6a9231127f4355d351130a0b94e663d5d9dc8b3a54ca17d83de49"
dependencies = [
"digest 0.10.6",
"ed25519-zebra",
@ -227,18 +239,18 @@ dependencies = [
[[package]]
name = "cosmwasm-derive"
version = "1.2.5"
version = "1.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c9f7f0e51bfc7295f7b2664fe8513c966428642aa765dad8a74acdab5e0c773"
checksum = "a0fb2ce09f41a3dae1a234d56a9988f9aff4c76441cd50ef1ee9a4f20415b028"
dependencies = [
"syn 1.0.109",
]
[[package]]
name = "cosmwasm-schema"
version = "1.2.5"
version = "1.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f00b363610218eea83f24bbab09e1a7c3920b79f068334fdfcc62f6129ef9fc"
checksum = "230e5d1cefae5331db8934763c81b9c871db6a2cd899056a5694fa71d292c815"
dependencies = [
"cosmwasm-schema-derive",
"schemars",
@ -249,9 +261,9 @@ dependencies = [
[[package]]
name = "cosmwasm-schema-derive"
version = "1.2.5"
version = "1.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae38f909b2822d32b275c9e2db9728497aa33ffe67dd463bc67c6a3b7092785c"
checksum = "43dadf7c23406cb28079d69e6cb922c9c29b9157b0fe887e3b79c783b7d4bcb8"
dependencies = [
"proc-macro2",
"quote",
@ -260,9 +272,9 @@ dependencies = [
[[package]]
name = "cosmwasm-std"
version = "1.2.5"
version = "1.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a49b85345e811c8e80ec55d0d091e4fcb4f00f97ab058f9be5f614c444a730cb"
checksum = "4337eef8dfaf8572fe6b6b415d6ec25f9308c7bb09f2da63789209fb131363be"
dependencies = [
"base64",
"cosmwasm-crypto",
@ -482,8 +494,8 @@ dependencies = [
"anyhow",
"cosmwasm-std",
"cosmwasm-storage",
"cw-storage-plus",
"cw-utils",
"cw-storage-plus 0.13.4",
"cw-utils 0.13.4",
"derivative",
"itertools",
"prost",
@ -503,6 +515,17 @@ dependencies = [
"serde",
]
[[package]]
name = "cw-storage-plus"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f0e92a069d62067f3472c62e30adedb4cab1754725c0f2a682b3128d2bf3c79"
dependencies = [
"cosmwasm-std",
"schemars",
"serde",
]
[[package]]
name = "cw-utils"
version = "0.13.4"
@ -515,6 +538,21 @@ dependencies = [
"thiserror",
]
[[package]]
name = "cw-utils"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c80e93d1deccb8588db03945016a292c3c631e6325d349ebb35d2db6f4f946f7"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw2 1.1.0",
"schemars",
"semver",
"serde",
"thiserror",
]
[[package]]
name = "cw2"
version = "0.13.4"
@ -522,11 +560,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cf4639517490dd36b333bbd6c4fbd92e325fd0acf4683b41753bc5eb63bfc1"
dependencies = [
"cosmwasm-std",
"cw-storage-plus",
"cw-storage-plus 0.13.4",
"schemars",
"serde",
]
[[package]]
name = "cw2"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29ac2dc7a55ad64173ca1e0a46697c31b7a5c51342f55a1e84a724da4eb99908"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-storage-plus 1.1.0",
"schemars",
"serde",
"thiserror",
]
[[package]]
name = "cw20"
version = "0.13.4"
@ -534,7 +586,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cb782b8f110819a4eb5dbbcfed25ffba49ec16bbe32b4ad8da50a5ce68fec05"
dependencies = [
"cosmwasm-std",
"cw-utils",
"cw-utils 0.13.4",
"schemars",
"serde",
]
@ -546,9 +598,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0306e606581f4fb45e82bcbb7f0333179ed53dd949c6523f01a99b4bfc1475a0"
dependencies = [
"cosmwasm-std",
"cw-storage-plus",
"cw-utils",
"cw2",
"cw-storage-plus 0.13.4",
"cw-utils 0.13.4",
"cw2 0.13.4",
"cw20",
"schemars",
"serde",
@ -561,7 +613,7 @@ version = "0.1.0"
dependencies = [
"cosmwasm-std",
"cosmwasm-storage",
"cw2",
"cw2 0.13.4",
"cw20",
"cw20-base",
"schemars",
@ -601,7 +653,7 @@ dependencies = [
"ident_case",
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.23",
]
[[package]]
@ -612,7 +664,7 @@ checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a"
dependencies = [
"darling_core",
"quote",
"syn 2.0.15",
"syn 2.0.23",
]
[[package]]
@ -779,7 +831,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.23",
]
[[package]]
@ -888,8 +940,8 @@ dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-multi-test",
"cw-storage-plus",
"cw2",
"cw-storage-plus 0.13.4",
"cw2 0.13.4",
"cw_transcode",
"hex",
"schemars",
@ -964,6 +1016,28 @@ dependencies = [
"digest 0.10.6",
]
[[package]]
name = "ibc-translator"
version = "0.1.0"
dependencies = [
"anybuf",
"anyhow",
"bs58",
"cosmwasm-schema",
"cosmwasm-std",
"cw-storage-plus 0.13.4",
"cw-utils 1.0.1",
"cw20",
"cw20-base",
"cw20-wrapped-2",
"serde-json-wasm 0.5.1",
"serde_wormhole",
"token-bridge-cosmwasm",
"wormhole-bindings",
"wormhole-cosmwasm",
"wormhole-sdk",
]
[[package]]
name = "ident_case"
version = "1.0.1"
@ -1273,9 +1347,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.56"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb"
dependencies = [
"unicode-ident",
]
@ -1334,9 +1408,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.26"
version = "1.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105"
dependencies = [
"proc-macro2",
]
@ -1557,9 +1631,9 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
[[package]]
name = "serde"
version = "1.0.160"
version = "1.0.164"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c"
checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d"
dependencies = [
"serde_derive",
]
@ -1593,13 +1667,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.160"
version = "1.0.164"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df"
checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.23",
]
[[package]]
@ -1758,9 +1832,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.15"
version = "2.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822"
checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737"
dependencies = [
"proc-macro2",
"quote",
@ -1817,7 +1891,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.23",
]
[[package]]
@ -1876,7 +1950,7 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.23",
]
[[package]]
@ -2461,7 +2535,7 @@ dependencies = [
"anyhow",
"cosmwasm-schema",
"cosmwasm-std",
"cw-storage-plus",
"cw-storage-plus 0.13.4",
"semver",
"serde_wormhole",
"thiserror",
@ -2506,7 +2580,7 @@ dependencies = [
"anyhow",
"cosmwasm-schema",
"cosmwasm-std",
"cw-storage-plus",
"cw-storage-plus 0.13.4",
"hex",
"schemars",
"semver",

View File

@ -11,7 +11,8 @@ members = [
"packages/wormhole-bindings",
"packages/cw_transcode",
"contracts/wormhole-ibc",
"contracts/wormchain-ibc-receiver"
"contracts/wormchain-ibc-receiver",
"contracts/ibc-translator"
]
# Needed to prevent unwanted feature unification between normal builds and dev builds. See
@ -40,4 +41,4 @@ 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" }
wormhole-ibc = { path = "contracts/wormhole-ibc" }

View File

@ -2,7 +2,7 @@
# 1. The first stage builds the contracts
# 2. The second is an empty image with only the wasm files (useful for exporting)
# 3. The third creates a node.js environment to deploy the contracts to devnet
FROM cosmwasm/workspace-optimizer:0.12.6@sha256:e6565a5e87c830ef3e8775a9035006b38ad0aaf0a96319158c802457b1dd1d08 AS builder
FROM cosmwasm/workspace-optimizer:0.13.0@sha256:d868e239f73fb45ba98dd088c0a6a15effd0b87b7b193701f02c3913ecb8a196 AS builder
COPY cosmwasm/Cargo.lock /code/
COPY cosmwasm/Cargo.toml /code/

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,32 @@
[package]
name = "ibc-translator"
version = "0.1.0"
authors = ["Wormhole Project Contributors"]
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
# for more explicit tests, cargo test --features=backtraces
backtraces = ["cosmwasm-std/backtraces"]
# use library feature to disable all instantiate/execute/query/reply exports
library = []
[dependencies]
anybuf = "0.1.0"
anyhow = "1"
bs58 = "0.4.0"
cosmwasm-schema = "1.2.7"
cosmwasm-std = {version="1.2.7", features = ["ibc3"] }
cw-storage-plus = "0.13.2"
cw-utils = "1.0.1"
cw20 = "0.13.2"
cw20-base = { version = "0.13.2", features = ["library"] }
cw20-wrapped-2 = { version = "0.1.0", features = ["library"] }
serde-json-wasm = "0.5.1"
serde_wormhole = "0.1.0"
token-bridge-cosmwasm = { version = "0.1.0", features = ["library"] }
wormhole-bindings = "0.1.0"
wormhole-cosmwasm = { version = "0.1.0", features = ["library"] }
wormhole-sdk = { version = "0.1.0", features = ["schemars"] }

View File

@ -0,0 +1,107 @@
#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use anyhow::{bail, Context};
use cosmwasm_std::{
to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Reply, Response, StdResult,
};
use wormhole_bindings::{tokenfactory::TokenFactoryMsg, WormholeQuery};
use crate::{
execute::{
complete_transfer_and_convert, convert_and_transfer, submit_update_chain_to_channel_map,
TransferType,
},
msg::{ExecuteMsg, InstantiateMsg, QueryMsg, COMPLETE_TRANSFER_REPLY_ID},
query::query_ibc_channel,
reply::handle_complete_transfer_reply,
state::TOKEN_BRIDGE_CONTRACT,
};
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, anyhow::Error> {
TOKEN_BRIDGE_CONTRACT
.save(deps.storage, &msg.token_bridge_contract)
.context("failed to save token bridge contract address to storage")?;
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<TokenFactoryMsg>, anyhow::Error> {
match msg {
ExecuteMsg::CompleteTransferAndConvert { vaa } => {
complete_transfer_and_convert(deps, env, info, vaa)
}
ExecuteMsg::GatewayConvertAndTransfer {
recipient,
chain,
fee,
nonce,
} => convert_and_transfer(
deps,
info,
env,
recipient,
chain,
TransferType::Simple { fee },
nonce,
),
ExecuteMsg::GatewayConvertAndTransferWithPayload {
contract,
chain,
payload,
nonce,
} => convert_and_transfer(
deps,
info,
env,
contract,
chain,
TransferType::ContractControlled { payload },
nonce,
),
ExecuteMsg::SubmitUpdateChainToChannelMap { vaa } => {
submit_update_chain_to_channel_map(deps, vaa)
}
}
}
/// Reply handler for various kinds of replies
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn reply(
deps: DepsMut,
env: Env,
msg: Reply,
) -> Result<Response<TokenFactoryMsg>, anyhow::Error> {
if msg.id == COMPLETE_TRANSFER_REPLY_ID {
return handle_complete_transfer_reply(deps, env, msg);
}
// for safety, let's error out if we don't match a reply ID
bail!("unmatched reply id {}", msg.id);
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::IbcChannel { chain_id } => to_binary(&query_ibc_channel(deps, chain_id)?),
}
}

View File

@ -0,0 +1,310 @@
use anyhow::{bail, ensure, Context};
use cosmwasm_std::{
to_binary, Binary, Coin, CosmosMsg, Deps, DepsMut, Empty, Env, Event, MessageInfo,
QueryRequest, Response, SubMsg, Uint128, WasmMsg, WasmQuery,
};
use cw_token_bridge::msg::{
Asset, AssetInfo, ExecuteMsg as TokenBridgeExecuteMsg, QueryMsg as TokenBridgeQueryMsg,
TransferInfoResponse,
};
use cw_wormhole::byte_utils::ByteUtils;
use cw20_wrapped_2::msg::ExecuteMsg as Cw20WrappedExecuteMsg;
use serde_wormhole::RawMessage;
use std::str;
use wormhole_bindings::{
tokenfactory::{TokenFactoryMsg, TokenMsg},
WormholeQuery,
};
use wormhole_sdk::{
ibc_translator::{Action, GovernancePacket},
vaa::{Body, Header},
Chain,
};
use crate::{
msg::COMPLETE_TRANSFER_REPLY_ID,
state::{
CHAIN_TO_CHANNEL_MAP, CURRENT_TRANSFER, CW_DENOMS, TOKEN_BRIDGE_CONTRACT, VAA_ARCHIVE,
},
};
pub enum TransferType {
Simple { fee: Uint128 },
ContractControlled { payload: Binary },
}
/// Calls into the wormhole token bridge to complete the payload3 transfer.
pub fn complete_transfer_and_convert(
deps: DepsMut<WormholeQuery>,
env: Env,
info: MessageInfo,
vaa: Binary,
) -> Result<Response<TokenFactoryMsg>, anyhow::Error> {
// get the token bridge contract address from storage
let token_bridge_contract = TOKEN_BRIDGE_CONTRACT
.load(deps.storage)
.context("could not load token bridge contract address")?;
// craft the token bridge execute message
// this will be added as a submessage to the response
let token_bridge_execute_msg = to_binary(&TokenBridgeExecuteMsg::CompleteTransferWithPayload {
data: vaa.clone(),
relayer: info.sender.to_string(),
})
.context("could not serialize token bridge execute msg")?;
let sub_msg = SubMsg::reply_on_success(
CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: token_bridge_contract.clone(),
msg: token_bridge_execute_msg,
funds: vec![],
}),
COMPLETE_TRANSFER_REPLY_ID,
);
// craft the token bridge query message to parse the payload3 vaa
let token_bridge_query_msg = to_binary(&TokenBridgeQueryMsg::TransferInfo { vaa })
.context("could not serialize token bridge transfer_info query msg")?;
let transfer_info: TransferInfoResponse = deps
.querier
.query(&QueryRequest::Wasm(WasmQuery::Smart {
contract_addr: token_bridge_contract,
msg: token_bridge_query_msg,
}))
.context("could not parse token bridge payload3 vaa")?;
// DEFENSE IN-DEPTH CHECK FOR PAYLOAD3 VAAs
// ensure that the transfer vaa recipient is this contract.
// we should never process any VAAs that are not directed to this contract.
let target_address = (&transfer_info.recipient.as_slice()).get_address(0);
let recipient = deps.api.addr_humanize(&target_address)?;
ensure!(
recipient == env.contract.address,
"vaa recipient must be this contract"
);
// save interim state
CURRENT_TRANSFER
.save(deps.storage, &transfer_info)
.context("failed to save current transfer to storage")?;
// return the response which will callback to the reply handler on success
Ok(Response::new()
.add_submessage(sub_msg)
.add_attribute("action", "complete_transfer_with_payload")
.add_attribute(
"transfer_payload",
Binary::from(transfer_info.payload).to_base64(),
))
}
pub fn convert_and_transfer(
deps: DepsMut<WormholeQuery>,
info: MessageInfo,
env: Env,
recipient: Binary,
chain: u16,
transfer_type: TransferType,
nonce: u32,
) -> Result<Response<TokenFactoryMsg>, anyhow::Error> {
// load the token bridge contract address
let token_bridge_contract = TOKEN_BRIDGE_CONTRACT
.load(deps.storage)
.context("could not load token bridge contract address")?;
ensure!(info.funds.len() == 1, "no bridging coin included");
let bridging_coin = info.funds[0].clone();
let cw20_contract_addr = parse_bank_token_factory_contract(deps, env, bridging_coin.clone())?;
// batch calls together
let mut response: Response<TokenFactoryMsg> = Response::new();
// 1. tokenfactorymsg::burn for the bank tokens
response = response.add_message(TokenMsg::BurnTokens {
denom: bridging_coin.denom.clone(),
amount: bridging_coin.amount.u128(),
burn_from_address: "".to_string(),
});
// 2. cw20::increaseAllowance to the contract address for the token bridge to spend the amount of tokens
let increase_allowance_msg = to_binary(&Cw20WrappedExecuteMsg::IncreaseAllowance {
spender: token_bridge_contract.clone(),
amount: bridging_coin.amount,
expires: None,
})
.context("could not serialize cw20 increase_allowance msg")?;
response = response.add_message(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: cw20_contract_addr.clone(),
msg: increase_allowance_msg,
funds: vec![],
}));
// 3. token_bridge::initiate_transfer -- the cw20 tokens will be either burned or transferred to the token_bridge
let token_bridge_transfer: TokenBridgeExecuteMsg = match transfer_type {
TransferType::Simple { fee } => TokenBridgeExecuteMsg::InitiateTransfer {
asset: Asset {
info: AssetInfo::Token {
contract_addr: cw20_contract_addr,
},
amount: bridging_coin.amount,
},
recipient_chain: chain,
recipient,
fee,
nonce,
},
TransferType::ContractControlled { payload } => {
TokenBridgeExecuteMsg::InitiateTransferWithPayload {
asset: Asset {
info: AssetInfo::Token {
contract_addr: cw20_contract_addr,
},
amount: bridging_coin.amount,
},
recipient_chain: chain,
recipient,
fee: Uint128::from(0u128),
payload,
nonce,
}
}
};
let initiate_transfer_msg = to_binary(&token_bridge_transfer)
.context("could not serialize token bridge initiate_transfer msg")?;
response = response.add_message(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: token_bridge_contract,
msg: initiate_transfer_msg,
funds: vec![],
}));
Ok(response)
}
pub fn parse_bank_token_factory_contract(
deps: DepsMut<WormholeQuery>,
env: Env,
coin: Coin,
) -> Result<String, anyhow::Error> {
// extract the contract address from the denom of the token that was sent to us
// if the token is not a factory token created by this contract, return error
let parsed_denom = coin.denom.split('/').collect::<Vec<_>>();
ensure!(
parsed_denom.len() == 3
&& parsed_denom[0] == "factory"
&& parsed_denom[1] == env.contract.address,
"coin is not from the token factory"
);
// decode subdenom from base64 => encode as cosmos addr to get contract addr
let cw20_contract_addr = contract_addr_from_base58(deps.as_ref(), parsed_denom[2])?;
// validate that the contract does indeed match the stored denom we have for it
let stored_denom = CW_DENOMS
.load(deps.storage, cw20_contract_addr.clone())
.context(
"a corresponding denom for the extracted contract addr is not contained in storage",
)?;
ensure!(
stored_denom == coin.denom,
"the stored denom for the contract does not match the actual coin denom"
);
Ok(cw20_contract_addr)
}
pub fn contract_addr_from_base58(
deps: Deps<WormholeQuery>,
subdenom: &str,
) -> Result<String, anyhow::Error> {
let decoded_addr = bs58::decode(subdenom)
.into_vec()
.context(format!("failed to decode base58 subdenom {subdenom}"))?;
let canonical_addr = Binary::from(decoded_addr);
deps.api
.addr_humanize(&canonical_addr.into())
.map(|a| a.to_string())
.context(format!("failed to humanize cosmos address {subdenom}"))
}
pub fn submit_update_chain_to_channel_map(
deps: DepsMut<WormholeQuery>,
vaa: Binary,
) -> Result<Response<TokenFactoryMsg>, anyhow::Error> {
// 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("failed to verify vaa")?;
// 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 || govpacket.chain == Chain::Any,
"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 ibc-translator 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
CHAIN_TO_CHANNEL_MAP
.save(
deps.storage,
chain_id.into(),
&channel_id_trimmed.to_string(),
)
.context("failed to save channel chain")?;
Ok(Response::new().add_event(
Event::new("UpdateChainToChannelMap")
.add_attribute("chain_id", chain_id.to_string())
.add_attribute("channel_id", channel_id_trimmed),
))
}
}
}

View File

@ -0,0 +1,6 @@
pub mod contract;
pub mod execute;
pub mod msg;
pub mod query;
pub mod reply;
pub mod state;

View File

@ -0,0 +1,86 @@
use cosmwasm_schema::{cw_serde, QueryResponses};
use cosmwasm_std::{Binary, Uint128};
pub const COMPLETE_TRANSFER_REPLY_ID: u64 = 1;
#[cw_serde]
pub struct InstantiateMsg {
pub token_bridge_contract: String,
}
#[cw_serde]
pub enum ExecuteMsg {
/// Submit a VAA to complete a wormhole payload3 token bridge transfer.
/// This function will:
/// 1. complete the wormhole token bridge transfer.
/// 2. Lock the newly minted cw20 tokens.
/// 3. CreateDenom (if it doesn't already exist)
/// 4. Mint an equivalent amount of bank tokens using the token factory.
/// 5. Send the minted bank tokens to the destination address with contract payload if applicable.
CompleteTransferAndConvert {
/// VAA to submit. The VAA should be encoded in the standard wormhole
/// wire format.
vaa: Binary,
},
/// Convert bank tokens into the equivalent (locked) cw20 tokens and trigger a wormhole token bridge transfer.
/// This function will:
/// 1. Validate that the bank tokens originated from cw20 tokens that are locked in this contract.
/// 2. Burn the bank tokens using the token factory.
/// 3. Unlock the equivalent cw20 tokens.
/// 4. Cross-call into the wormhole token bridge to initiate a cross-chain transfer with a gateway transfer payload.
GatewayConvertAndTransfer {
recipient: Binary,
chain: u16,
fee: Uint128,
nonce: u32,
},
/// Convert bank tokens into the equivalent (locked) cw20 tokens and trigger a wormhole token bridge transfer.
/// This function will:
/// 1. Validate that the bank tokens originated from cw20 tokens that are locked in this contract.
/// 2. Burn the bank tokens using the token factory.
/// 3. Unlock the equivalent cw20 tokens.
/// 4. Cross-call into the wormhole token bridge to initiate a cross-chain transfer with a gateway transfer-with-payload payload.
GatewayConvertAndTransferWithPayload {
contract: Binary,
chain: u16,
payload: Binary,
nonce: u32,
},
/// Submit a signed VAA to update the on-chain state.
SubmitUpdateChainToChannelMap {
/// VAA to submit. The VAA should be encoded in the standard wormhole
/// wire format.
vaa: Binary,
},
}
#[cw_serde]
#[derive(QueryResponses)]
pub enum QueryMsg {
#[returns(ChannelResponse)]
IbcChannel { chain_id: u16 },
}
#[cw_serde]
pub struct ChannelResponse {
pub channel: String,
}
#[cw_serde]
pub enum GatewayIbcTokenBridgePayload {
GatewayTransfer {
chain: u16,
recipient: Binary,
fee: u128,
nonce: u32,
},
GatewayTransferWithPayload {
chain: u16,
contract: Binary,
payload: Binary,
nonce: u32,
},
}

View File

@ -0,0 +1,9 @@
use cosmwasm_std::{Deps, StdResult};
use crate::{msg::ChannelResponse, state::CHAIN_TO_CHANNEL_MAP};
pub fn query_ibc_channel(deps: Deps, chain_id: u16) -> StdResult<ChannelResponse> {
let channel = CHAIN_TO_CHANNEL_MAP.load(deps.storage, chain_id)?;
Ok(ChannelResponse { channel })
}

View File

@ -0,0 +1,213 @@
use crate::{
msg::GatewayIbcTokenBridgePayload,
state::{CHAIN_TO_CHANNEL_MAP, CURRENT_TRANSFER, CW_DENOMS},
};
use anybuf::Anybuf;
use anyhow::{ensure, Context};
use cosmwasm_std::{
from_binary, to_binary, Binary, CosmosMsg::Stargate, Deps, DepsMut, Env, QueryRequest, Reply,
Response, SubMsg, WasmQuery,
};
use cw20::TokenInfoResponse;
use cw20_base::msg::QueryMsg as TokenQuery;
use cw_token_bridge::msg::CompleteTransferResponse;
use wormhole_bindings::tokenfactory::{DenomUnit, Metadata, TokenFactoryMsg, TokenMsg};
pub fn handle_complete_transfer_reply(
deps: DepsMut,
env: Env,
msg: Reply,
) -> Result<Response<TokenFactoryMsg>, anyhow::Error> {
// we should only be replying on success
ensure!(
msg.result.is_ok(),
"msg result is not okay, we should never get here"
);
let res_data_raw = cw_utils::parse_reply_execute_data(msg)
.context("failed to parse protobuf reply response_data")?
.data
.context("no data in the response, we should never get here")?;
let res_data: CompleteTransferResponse =
from_binary(&res_data_raw).context("failed to deserialize response data")?;
let contract_addr = res_data
.contract
.context("no contract in response, we should never get here")?;
// load interim state
let transfer_info = CURRENT_TRANSFER
.load(deps.storage)
.context("failed to load current transfer from storage")?;
// delete interim state
CURRENT_TRANSFER.remove(deps.storage);
// deserialize payload into the type we expect
let payload: GatewayIbcTokenBridgePayload = serde_json_wasm::from_slice(&transfer_info.payload)
.context("failed to deserialize transfer payload")?;
match payload {
GatewayIbcTokenBridgePayload::GatewayTransfer {
chain,
recipient,
fee: _,
nonce: _,
} => {
let recipient_decoded = String::from_utf8(recipient.to_vec())
.context(format!("failed to convert {recipient} to utf8 string"))?;
convert_cw20_to_bank_and_send(
deps,
env,
recipient_decoded,
res_data.amount.into(),
contract_addr,
chain,
None,
)
}
GatewayIbcTokenBridgePayload::GatewayTransferWithPayload {
chain,
contract,
payload,
nonce: _,
} => {
let contract_decoded = String::from_utf8(contract.to_vec())
.context(format!("failed to convert {contract} to utf8 string"))?;
convert_cw20_to_bank_and_send(
deps,
env,
contract_decoded,
res_data.amount.into(),
contract_addr,
chain,
Some(payload),
)
}
}
}
pub fn convert_cw20_to_bank_and_send(
deps: DepsMut,
env: Env,
recipient: String,
amount: u128,
cw20_contract_addr: String,
chain_id: u16,
payload: Option<Binary>,
) -> Result<Response<TokenFactoryMsg>, anyhow::Error> {
deps.api
.addr_validate(&cw20_contract_addr)
.context(format!("invalid contract address {cw20_contract_addr}"))?;
// convert contract address into base64
let subdenom = contract_addr_to_base58(deps.as_ref(), cw20_contract_addr.clone())?;
// format the token factory denom
let tokenfactory_denom = "factory/".to_string()
+ env.contract.address.to_string().as_ref()
+ "/"
+ subdenom.as_ref();
let mut response: Response<TokenFactoryMsg> = Response::new();
// check contract storage see if we've created a denom for this cw20 token yet
// if we haven't created the denom, then create the denom
if !CW_DENOMS.has(deps.storage, cw20_contract_addr.clone()) {
// call into the cw20 contract to get the token's metadata
let request = QueryRequest::Wasm(WasmQuery::Smart {
contract_addr: cw20_contract_addr.clone(),
msg: to_binary(&TokenQuery::TokenInfo {})?,
});
let token_info: TokenInfoResponse = deps.querier.query(&request)?;
// Populate token factory token's metadata from cw20 token's metadata
let tf_description = token_info.name.clone()
+ ", "
+ token_info.symbol.as_str()
+ ", "
+ tokenfactory_denom.as_str();
let tf_denom_unit_base = DenomUnit {
denom: tokenfactory_denom.clone(),
exponent: 0,
aliases: vec![],
};
let tf_scaled_denom = "wormhole/".to_string()
+ subdenom.as_str()
+ "/"
+ token_info.decimals.to_string().as_str();
let tf_denom_unit_scaled = DenomUnit {
denom: tf_scaled_denom,
exponent: u32::from(token_info.decimals),
aliases: vec![],
};
let tf_metadata = Metadata {
description: Some(tf_description),
base: Some(tokenfactory_denom.clone()),
denom_units: vec![tf_denom_unit_base, tf_denom_unit_scaled],
display: Some(tokenfactory_denom.clone()),
name: Some(token_info.name),
symbol: Some(token_info.symbol),
};
// call into token factory to create the denom
let create_denom = SubMsg::new(TokenMsg::CreateDenom {
subdenom,
metadata: Some(tf_metadata),
});
response = response.add_submessage(create_denom);
// add the contract_addr => tokenfactory denom to storage
CW_DENOMS
.save(deps.storage, cw20_contract_addr, &tokenfactory_denom)
.context("failed to save contract_addr => tokenfactory denom to storage")?;
}
// add calls to mint and send bank tokens
response = response.add_message(TokenMsg::MintTokens {
denom: tokenfactory_denom.clone(),
amount,
mint_to_address: env.contract.address.to_string(),
});
let channel = CHAIN_TO_CHANNEL_MAP
.load(deps.storage, chain_id)
.context("chain id does not have an allowed channel")?;
let payload_decoded = match payload {
Some(payload) => String::from_utf8(payload.to_vec())
.context(format!("failed to convert {payload} to utf8 string"))?,
None => "".to_string(),
};
// Create MsgTransfer protobuf message for Stargate
// https://github.com/cosmos/ibc-go/blob/main/proto/ibc/applications/transfer/v1/tx.proto#L27
// TimeoutTimestamp is 14 days from now which is the trusting period of the counterparty light client
let ibc_msg_transfer = Anybuf::new()
.append_string(1, "transfer") // source port
.append_string(2, channel) // source channel
.append_message(
3,
&Anybuf::new()
.append_string(1, tokenfactory_denom)
.append_string(2, amount.to_string()),
) // Token
.append_string(4, env.contract.address) // sender
.append_string(5, recipient) // receiver
.append_message(6, &Anybuf::new().append_uint64(1, 0).append_uint64(2, 0)) // TimeoutHeight
.append_uint64(7, env.block.time.plus_days(14).nanos()) // TimeoutTimestamp
.append_string(8, payload_decoded); // Memo
response = response.add_message(Stargate {
type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(),
value: ibc_msg_transfer.into_vec().into(),
});
Ok(response)
}
// Base58 allows the subdenom to be a maximum of 44 bytes (max subdenom length) for up to a 32 byte address
fn contract_addr_to_base58(deps: Deps, contract_addr: String) -> Result<String, anyhow::Error> {
// convert the contract address into bytes
let contract_addr_bytes = deps.api.addr_canonicalize(&contract_addr).context(format!(
"could not canonicalize contract address {contract_addr}"
))?;
let base_58_addr = bs58::encode(contract_addr_bytes.as_slice()).into_string();
Ok(base_58_addr)
}

View File

@ -0,0 +1,14 @@
use cw_storage_plus::{Item, Map};
use cw_token_bridge::msg::TransferInfoResponse;
pub const TOKEN_BRIDGE_CONTRACT: Item<String> = Item::new("token_bridge_contract");
// Holds temp state for the wormhole message that the contract is currently processing
pub const CURRENT_TRANSFER: Item<TransferInfoResponse> = Item::new("current_transfer");
// Maps cw20 address -> bank token denom
pub const CW_DENOMS: Map<String, String> = Map::new("cw_denoms");
pub const CHAIN_TO_CHANNEL_MAP: Map<u16, String> = Map::new("chain_to_channel_map");
pub const VAA_ARCHIVE: Map<&[u8], bool> = Map::new("vaa_archive");

View File

@ -26,16 +26,14 @@ pub fn ibc_channel_open(
if channel.version.as_str() != IBC_APP_VERSION {
return Err(StdError::generic_err(format!(
"Must set version to `{}`",
IBC_APP_VERSION
"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
"Counterparty version must be `{IBC_APP_VERSION}`"
)));
}
}
@ -81,7 +79,7 @@ pub fn ibc_packet_receive(
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));
let acknowledgement = encode_ibc_error(format!("invalid packet: {e}"));
Ok(IbcReceiveResponse::new()
.set_ack(acknowledgement)
.add_attribute("action", "ibc_packet_ack"))

View File

@ -27,16 +27,14 @@ pub fn ibc_channel_open(
if channel.version.as_str() != IBC_APP_VERSION {
return Err(StdError::generic_err(format!(
"Must set version to `{}`",
IBC_APP_VERSION
"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
"Counterparty version must be `{IBC_APP_VERSION}`"
)));
}
}

View File

@ -1,6 +1,7 @@
#[cfg(feature = "fake")]
pub mod fake;
mod query;
pub mod tokenfactory;
pub use query::*;

View File

@ -0,0 +1,134 @@
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{CosmosMsg, CustomMsg};
/// A top-level Custom message for the token factory.
/// It is embedded like this to easily allow adding other variants that are custom
/// to your chain, or other "standardized" extensions along side it.
#[cw_serde]
pub enum TokenFactoryMsg {
Token(TokenMsg),
}
/// Special messages to be supported by any chain that supports token_factory
#[cw_serde]
pub enum TokenMsg {
/// CreateDenom creates a new factory denom, of denomination:
/// factory/{creating contract bech32 address}/{Subdenom}
/// Subdenom can be of length at most 44 characters, in [0-9a-zA-Z./]
/// Empty subdenoms are valid.
/// The (creating contract address, subdenom) pair must be unique.
/// The created denom's admin is the creating contract address,
/// but this admin can be changed using the UpdateAdmin binding.
///
/// If you set an initial metadata here, this is equivalent
/// to calling SetMetadata directly on the returned denom.
CreateDenom {
subdenom: String,
metadata: Option<Metadata>,
},
/// ChangeAdmin changes the admin for a factory denom.
/// Can only be called by the current contract admin.
/// If the NewAdminAddress is empty, the denom will have no admin.
ChangeAdmin {
denom: String,
new_admin_address: String,
},
/// Contracts can mint native tokens for an existing factory denom
/// that they are the admin of.
MintTokens {
denom: String,
amount: u128,
mint_to_address: String,
},
/// Contracts can burn native tokens for an existing factory denom
/// that they are the admin of.
BurnTokens {
denom: String,
amount: u128,
burn_from_address: String,
},
/// Contracts can force transfer tokens for an existing factory denom
/// that they are the admin of.
ForceTransfer {
denom: String,
amount: u128,
from_address: String,
to_address: String,
},
SetMetadata {
denom: String,
metadata: Metadata,
},
}
impl TokenMsg {
pub fn mint_contract_tokens(denom: String, amount: u128, mint_to_address: String) -> Self {
TokenMsg::MintTokens {
denom,
amount,
mint_to_address,
}
}
pub fn burn_contract_tokens(denom: String, amount: u128, burn_from_address: String) -> Self {
TokenMsg::BurnTokens {
denom,
amount,
burn_from_address,
}
}
pub fn force_transfer_tokens(
denom: String,
amount: u128,
from_address: String,
to_address: String,
) -> Self {
TokenMsg::ForceTransfer {
denom,
amount,
from_address,
to_address,
}
}
}
impl From<TokenMsg> for CosmosMsg<TokenFactoryMsg> {
fn from(msg: TokenMsg) -> CosmosMsg<TokenFactoryMsg> {
CosmosMsg::Custom(TokenFactoryMsg::Token(msg))
}
}
impl CustomMsg for TokenFactoryMsg {}
/// This maps to cosmos.bank.v1beta1.Metadata protobuf struct
#[cw_serde]
pub struct Metadata {
pub description: Option<String>,
/// denom_units represents the list of DenomUnit's for a given coin
pub denom_units: Vec<DenomUnit>,
/// base represents the base denom (should be the DenomUnit with exponent = 0).
pub base: Option<String>,
/// display indicates the suggested denom that should be displayed in clients.
pub display: Option<String>,
/// name defines the name of the token (eg: Cosmos Atom)
pub name: Option<String>,
/// symbol is the token symbol usually shown on exchanges (eg: ATOM). This can
/// be the same as the display.
pub symbol: Option<String>,
}
/// This maps to cosmos.bank.v1beta1.DenomUnit protobuf struct
#[cw_serde]
pub struct DenomUnit {
/// denom represents the string name of the given denom unit (e.g uatom).
pub denom: String,
/// exponent represents power of 10 exponent that one must
/// raise the base_denom to in order to equal the given DenomUnit's denom
/// 1 denom = 1^exponent base_denom
/// (e.g. with a base_denom of uatom, one can create a DenomUnit of 'atom' with
/// exponent = 6, thus: 1 atom = 10^6 uatom).
pub exponent: u32,
/// aliases is a list of string aliases for the given denom
pub aliases: Vec<String>,
}

View File

@ -0,0 +1,332 @@
use serde::{Deserialize, Serialize};
use crate::Chain;
/// Represents a governance action targeted at the wormchain ibc translator contract.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Action {
#[serde(rename = "1")]
UpdateChannelChain {
// an existing IBC channel ID
#[serde(with = "crate::serde_array")]
channel_id: [u8; 64],
// the chain associated with this IBC channel_id
chain_id: Chain,
},
}
// MODULE = "IbcTranslator"
pub const MODULE: [u8; 32] =
*b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00IbcTranslator";
/// Represents the payload for a governance VAA targeted at the wormchain ibc translator contract.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GovernancePacket {
/// Describes the chain on which the governance action should be carried out.
pub chain: Chain,
/// The actual governance action to be carried out.
pub action: Action,
}
mod governance_packet_impl {
use std::fmt;
use serde::{
de::{Error, MapAccess, SeqAccess, Visitor},
ser::SerializeStruct,
Deserialize, Deserializer, Serialize, Serializer,
};
use crate::{
ibc_translator::{Action, GovernancePacket, MODULE},
Chain,
};
struct Module;
impl Serialize for Module {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
MODULE.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Module {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let arr = <[u8; 32]>::deserialize(deserializer)?;
if arr == MODULE {
Ok(Module)
} else {
Err(Error::custom(
"invalid governance module, expected \"IbcTranslator\"",
))
}
}
}
// governance actions
#[derive(Serialize, Deserialize)]
struct UpdateChannelChain {
#[serde(with = "crate::serde_array")]
channel_id: [u8; 64],
chain_id: Chain,
}
impl Serialize for GovernancePacket {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut seq = serializer.serialize_struct("GovernancePacket", 4)?;
seq.serialize_field("module", &Module)?;
// The wire format encodes the action before the chain and then appends the actual
// action payload.
match self.action.clone() {
Action::UpdateChannelChain {
channel_id,
chain_id,
} => {
seq.serialize_field("action", &1u8)?;
seq.serialize_field("chain", &self.chain)?;
seq.serialize_field(
"payload",
&UpdateChannelChain {
channel_id,
chain_id,
},
)?;
}
}
seq.end()
}
}
struct GovernancePacketVisitor;
impl<'de> Visitor<'de> for GovernancePacketVisitor {
type Value = GovernancePacket;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("struct GovernancePacket")
}
#[inline]
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
static EXPECTING: &str = "struct GovernancePacket with 4 elements";
let _: Module = seq
.next_element()?
.ok_or_else(|| Error::invalid_length(0, &EXPECTING))?;
let act: u8 = seq
.next_element()?
.ok_or_else(|| Error::invalid_length(1, &EXPECTING))?;
let chain = seq
.next_element()?
.ok_or_else(|| Error::invalid_length(2, &EXPECTING))?;
let action = match act {
1 => {
let UpdateChannelChain {
channel_id,
chain_id,
} = seq
.next_element()?
.ok_or_else(|| Error::invalid_length(3, &EXPECTING))?;
Action::UpdateChannelChain {
channel_id,
chain_id,
}
}
v => {
return Err(Error::custom(format_args!(
"invalid value: {v}, expected 1"
)))
}
};
Ok(GovernancePacket { chain, action })
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum Field {
Module,
Action,
Chain,
Payload,
}
let mut module = None;
let mut chain = None;
let mut action = None;
let mut payload = None;
while let Some(key) = map.next_key::<Field>()? {
match key {
Field::Module => {
if module.is_some() {
return Err(Error::duplicate_field("module"));
}
module = map.next_value::<Module>().map(Some)?;
}
Field::Action => {
if action.is_some() {
return Err(Error::duplicate_field("action"));
}
action = map.next_value::<u8>().map(Some)?;
}
Field::Chain => {
if chain.is_some() {
return Err(Error::duplicate_field("chain"));
}
chain = map.next_value().map(Some)?;
}
Field::Payload => {
if payload.is_some() {
return Err(Error::duplicate_field("payload"));
}
let a = action.as_ref().copied().ok_or_else(|| {
Error::custom("`action` must be known before deserializing `payload`")
})?;
let p = match a {
1 => {
let UpdateChannelChain {
channel_id,
chain_id,
} = map.next_value()?;
Action::UpdateChannelChain {
channel_id,
chain_id,
}
}
v => {
return Err(Error::custom(format_args!(
"invalid action: {v}, expected one of: 1, 2"
)))
}
};
payload = Some(p);
}
}
}
let _ = module.ok_or_else(|| Error::missing_field("module"))?;
let chain = chain.ok_or_else(|| Error::missing_field("chain"))?;
let action = payload.ok_or_else(|| Error::missing_field("payload"))?;
Ok(GovernancePacket { chain, action })
}
}
impl<'de> Deserialize<'de> for GovernancePacket {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
const FIELDS: &[&str] = &["module", "action", "chain", "payload"];
deserializer.deserialize_struct("GovernancePacket", FIELDS, GovernancePacketVisitor)
}
}
}
#[cfg(test)]
mod test {
use crate::{vaa::Signature, Chain, Vaa, GOVERNANCE_EMITTER};
use super::{Action, GovernancePacket};
#[test]
fn happy_path() {
let buf = [
// version
0x01, // guardian set index
0x00, // signatures
0x00, 0x00, 0x00, 0x01, 0x00, 0xb0, 0x72, 0x50, 0x5b, 0x5b, 0x99, 0x9c, 0x1d, 0x08,
0x90, 0x5c, 0x02, 0xe2, 0xb6, 0xb2, 0x83, 0x2e, 0xf7, 0x2c, 0x0b, 0xa6, 0xc8, 0xdb,
0x4f, 0x77, 0xfe, 0x45, 0x7e, 0xf2, 0xb3, 0xd0, 0x53, 0x41, 0x0b, 0x1e, 0x92, 0xa9,
0x19, 0x4d, 0x92, 0x10, 0xdf, 0x24, 0xd9, 0x87, 0xac, 0x83, 0xd7, 0xb6, 0xf0, 0xc2,
0x1c, 0xe9, 0x0f, 0x8b, 0xc1, 0x86, 0x9d, 0xe0, 0x89, 0x8b, 0xda, 0x7e, 0x98, 0x01,
// timestamp
0x00, 0x00, 0x00, 0x01, // nonce
0x00, 0x00, 0x00, 0x01, // emitter chain
0x00, 0x01, // emitter address
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x04, // sequence
0x00, 0x00, 0x00, 0x00, 0x01, 0x3c, 0x1b, 0xfa, // consistency
0x00, // module = "IbcTranslator"
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0x62, 0x63, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x6c,
0x61, 0x74, 0x6f, 0x72, // action (IbcReceiverActionUpdateChannelChain)
0x01, // target chain_id (unset)
0x00, 0x00, // IBC channel_id for the mapping ("channel-0")
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63,
0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x2d, 0x30, // IBC chain_id for the mapping
0x00, 0x13,
];
let channel_id_bytes: [u8; 64] =
*b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00channel-0";
let vaa = Vaa {
version: 1,
guardian_set_index: 0,
signatures: vec![Signature {
index: 0,
signature: [
0xb0, 0x72, 0x50, 0x5b, 0x5b, 0x99, 0x9c, 0x1d, 0x08, 0x90, 0x5c, 0x02, 0xe2,
0xb6, 0xb2, 0x83, 0x2e, 0xf7, 0x2c, 0x0b, 0xa6, 0xc8, 0xdb, 0x4f, 0x77, 0xfe,
0x45, 0x7e, 0xf2, 0xb3, 0xd0, 0x53, 0x41, 0x0b, 0x1e, 0x92, 0xa9, 0x19, 0x4d,
0x92, 0x10, 0xdf, 0x24, 0xd9, 0x87, 0xac, 0x83, 0xd7, 0xb6, 0xf0, 0xc2, 0x1c,
0xe9, 0x0f, 0x8b, 0xc1, 0x86, 0x9d, 0xe0, 0x89, 0x8b, 0xda, 0x7e, 0x98, 0x01,
],
}],
timestamp: 1,
nonce: 1,
emitter_chain: Chain::Solana,
emitter_address: GOVERNANCE_EMITTER,
sequence: 20_716_538,
consistency_level: 0,
payload: GovernancePacket {
chain: Chain::Any,
action: Action::UpdateChannelChain {
channel_id: channel_id_bytes,
chain_id: Chain::Injective,
},
},
};
assert_eq!(buf.as_ref(), &serde_wormhole::to_vec(&vaa).unwrap());
assert_eq!(vaa, serde_wormhole::from_slice(&buf).unwrap());
let encoded = serde_json::to_string(&vaa).unwrap();
assert_eq!(vaa, serde_json::from_str(&encoded).unwrap());
}
}

View File

@ -20,6 +20,7 @@ mod arraystring;
mod chain;
pub mod core;
pub mod ibc_receiver;
pub mod ibc_translator;
pub mod nft;
mod serde_array;
pub mod token;