瀏覽代碼

terra/contracts: add columbus-5 clone

Change-Id: I83c2387bd85f524962229e7b0ec72976f25d3396
Reisen 4 年之前
父節點
當前提交
ce6d92bb2b

+ 3 - 0
terra/contracts-5/README.md

@@ -0,0 +1,3 @@
+# Terra Wormhole Contracts
+
+The Wormhole Terra integration is developed and maintained by Everstake / @ysavchenko.

+ 5 - 0
terra/contracts-5/cw20-wrapped/.cargo/config

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

+ 27 - 0
terra/contracts-5/cw20-wrapped/Cargo.toml

@@ -0,0 +1,27 @@
+[package]
+name = "cw20-wrapped"
+version = "0.1.0"
+authors = ["Yuriy Savchenko <yuriy.savchenko@gmail.com>"]
+edition = "2018"
+description = "Wrapped CW20 token contract"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[features]
+backtraces = ["cosmwasm-std/backtraces"]
+# use library feature to disable all init/handle/query exports
+library = []
+
+[dependencies]
+cosmwasm-std = { version = "0.16.0" }
+cosmwasm-storage = { version = "0.16.0" }
+schemars = "0.8.1"
+serde = { version = "1.0.103", default-features = false, features = ["derive"] }
+cw20 = { version = "0.8.0" } 
+cw20-legacy = { version = "0.2.0", features = ["library"]} 
+cw-storage-plus  = { version = "0.8.0" }
+thiserror = { version = "1.0.20" }
+
+[dev-dependencies]
+cosmwasm-vm = { version = "0.16.0", default-features = false }

+ 359 - 0
terra/contracts-5/cw20-wrapped/src/contract.rs

@@ -0,0 +1,359 @@
+use cosmwasm_std::{
+    to_binary,
+    Api,
+    Binary,
+    CosmosMsg,
+    Env,
+    Extern,
+    HandleResponse,
+    HumanAddr,
+    InitResponse,
+    Querier,
+    StdError,
+    StdResult,
+    Storage,
+    Uint128,
+    WasmMsg,
+};
+
+use cw20_base::{
+    allowances::{
+        handle_burn_from,
+        handle_decrease_allowance,
+        handle_increase_allowance,
+        handle_send_from,
+        handle_transfer_from,
+        query_allowance,
+    },
+    contract::{
+        handle_mint,
+        handle_send,
+        handle_transfer,
+        query_balance,
+    },
+    state::{
+        token_info,
+        token_info_read,
+        MinterData,
+        TokenInfo,
+    },
+};
+
+use crate::{
+    msg::{
+        HandleMsg,
+        InitMsg,
+        QueryMsg,
+        WrappedAssetInfoResponse,
+    },
+    state::{
+        wrapped_asset_info,
+        wrapped_asset_info_read,
+        WrappedAssetInfo,
+    },
+};
+use cw20::TokenInfoResponse;
+use std::string::String;
+
+pub fn init<S: Storage, A: Api, Q: Querier>(
+    deps: &mut Extern<S, A, Q>,
+    env: Env,
+    msg: InitMsg,
+) -> StdResult<InitResponse> {
+    // store token info using cw20-base format
+    let data = TokenInfo {
+        name: msg.name,
+        symbol: msg.symbol,
+        decimals: msg.decimals,
+        total_supply: Uint128(0),
+        // set creator as minter
+        mint: Some(MinterData {
+            minter: deps.api.canonical_address(&env.message.sender)?,
+            cap: None,
+        }),
+    };
+    token_info(&mut deps.storage).save(&data)?;
+
+    // save wrapped asset info
+    let data = WrappedAssetInfo {
+        asset_chain: msg.asset_chain,
+        asset_address: msg.asset_address,
+        bridge: deps.api.canonical_address(&env.message.sender)?,
+    };
+    wrapped_asset_info(&mut deps.storage).save(&data)?;
+
+    if let Some(mint_info) = msg.mint {
+        handle_mint(deps, env, mint_info.recipient, mint_info.amount)?;
+    }
+
+    if let Some(hook) = msg.init_hook {
+        Ok(InitResponse {
+            messages: vec![CosmosMsg::Wasm(WasmMsg::Execute {
+                contract_addr: hook.contract_addr,
+                msg: hook.msg,
+                send: vec![],
+            })],
+            log: vec![],
+        })
+    } else {
+        Ok(InitResponse::default())
+    }
+}
+
+pub fn handle<S: Storage, A: Api, Q: Querier>(
+    deps: &mut Extern<S, A, Q>,
+    env: Env,
+    msg: HandleMsg,
+) -> StdResult<HandleResponse> {
+    match msg {
+        // these all come from cw20-base to implement the cw20 standard
+        HandleMsg::Transfer { recipient, amount } => {
+            Ok(handle_transfer(deps, env, recipient, amount)?)
+        }
+        HandleMsg::Burn { account, amount } => Ok(handle_burn_from(deps, env, account, amount)?),
+        HandleMsg::Send {
+            contract,
+            amount,
+            msg,
+        } => Ok(handle_send(deps, env, contract, amount, msg)?),
+        HandleMsg::Mint { recipient, amount } => handle_mint_wrapped(deps, env, recipient, amount),
+        HandleMsg::IncreaseAllowance {
+            spender,
+            amount,
+            expires,
+        } => Ok(handle_increase_allowance(
+            deps, env, spender, amount, expires,
+        )?),
+        HandleMsg::DecreaseAllowance {
+            spender,
+            amount,
+            expires,
+        } => Ok(handle_decrease_allowance(
+            deps, env, spender, amount, expires,
+        )?),
+        HandleMsg::TransferFrom {
+            owner,
+            recipient,
+            amount,
+        } => Ok(handle_transfer_from(deps, env, owner, recipient, amount)?),
+        HandleMsg::BurnFrom { owner, amount } => Ok(handle_burn_from(deps, env, owner, amount)?),
+        HandleMsg::SendFrom {
+            owner,
+            contract,
+            amount,
+            msg,
+        } => Ok(handle_send_from(deps, env, owner, contract, amount, msg)?),
+    }
+}
+
+fn handle_mint_wrapped<S: Storage, A: Api, Q: Querier>(
+    deps: &mut Extern<S, A, Q>,
+    env: Env,
+    recipient: HumanAddr,
+    amount: Uint128,
+) -> StdResult<HandleResponse> {
+    // Only bridge can mint
+    let wrapped_info = wrapped_asset_info_read(&deps.storage).load()?;
+    if wrapped_info.bridge != deps.api.canonical_address(&env.message.sender)? {
+        return Err(StdError::unauthorized());
+    }
+
+    Ok(handle_mint(deps, env, recipient, amount)?)
+}
+
+pub fn query<S: Storage, A: Api, Q: Querier>(
+    deps: &Extern<S, A, Q>,
+    msg: QueryMsg,
+) -> StdResult<Binary> {
+    match msg {
+        QueryMsg::WrappedAssetInfo {} => to_binary(&query_wrapped_asset_info(deps)?),
+        // inherited from cw20-base
+        QueryMsg::TokenInfo {} => to_binary(&query_token_info(deps)?),
+        QueryMsg::Balance { address } => to_binary(&query_balance(deps, address)?),
+        QueryMsg::Allowance { owner, spender } => {
+            to_binary(&query_allowance(deps, owner, spender)?)
+        }
+    }
+}
+
+pub fn query_token_info<S: Storage, A: Api, Q: Querier>(
+    deps: &Extern<S, A, Q>,
+) -> StdResult<TokenInfoResponse> {
+    let info = token_info_read(&deps.storage).load()?;
+    let res = TokenInfoResponse {
+        name: String::from("Wormhole:") + info.name.as_str(),
+        symbol: String::from("wh") + info.symbol.as_str(),
+        decimals: info.decimals,
+        total_supply: info.total_supply,
+    };
+    Ok(res)
+}
+
+pub fn query_wrapped_asset_info<S: Storage, A: Api, Q: Querier>(
+    deps: &Extern<S, A, Q>,
+) -> StdResult<WrappedAssetInfoResponse> {
+    let info = wrapped_asset_info_read(&deps.storage).load()?;
+    let res = WrappedAssetInfoResponse {
+        asset_chain: info.asset_chain,
+        asset_address: info.asset_address,
+        bridge: deps.api.human_address(&info.bridge)?,
+    };
+    Ok(res)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use cosmwasm_std::{
+        testing::{
+            mock_dependencies,
+            mock_env,
+        },
+        HumanAddr,
+    };
+    use cw20::TokenInfoResponse;
+
+    const CANONICAL_LENGTH: usize = 20;
+
+    fn get_balance<S: Storage, A: Api, Q: Querier, T: Into<HumanAddr>>(
+        deps: &Extern<S, A, Q>,
+        address: T,
+    ) -> Uint128 {
+        query_balance(&deps, address.into()).unwrap().balance
+    }
+
+    fn do_init<S: Storage, A: Api, Q: Querier>(deps: &mut Extern<S, A, Q>, creator: &HumanAddr) {
+        let init_msg = InitMsg {
+            asset_chain: 1,
+            asset_address: vec![1; 32].into(),
+            decimals: 10,
+            mint: None,
+            init_hook: None,
+        };
+        let env = mock_env(creator, &[]);
+        let res = init(deps, env, init_msg).unwrap();
+        assert_eq!(0, res.messages.len());
+
+        assert_eq!(
+            query_token_info(&deps).unwrap(),
+            TokenInfoResponse {
+                name: "Wormhole Wrapped".to_string(),
+                symbol: "WWT".to_string(),
+                decimals: 10,
+                total_supply: Uint128::from(0u128),
+            }
+        );
+
+        assert_eq!(
+            query_wrapped_asset_info(&deps).unwrap(),
+            WrappedAssetInfoResponse {
+                asset_chain: 1,
+                asset_address: vec![1; 32].into(),
+                bridge: creator.clone(),
+            }
+        );
+    }
+
+    fn do_init_and_mint<S: Storage, A: Api, Q: Querier>(
+        deps: &mut Extern<S, A, Q>,
+        creator: &HumanAddr,
+        mint_to: &HumanAddr,
+        amount: Uint128,
+    ) {
+        do_init(deps, creator);
+
+        let msg = HandleMsg::Mint {
+            recipient: mint_to.clone(),
+            amount,
+        };
+
+        let env = mock_env(&creator, &[]);
+        let res = handle(deps, env, msg.clone()).unwrap();
+        assert_eq!(0, res.messages.len());
+        assert_eq!(get_balance(deps, mint_to), amount);
+
+        assert_eq!(
+            query_token_info(&deps).unwrap(),
+            TokenInfoResponse {
+                name: "Wormhole Wrapped".to_string(),
+                symbol: "WWT".to_string(),
+                decimals: 10,
+                total_supply: amount,
+            }
+        );
+    }
+
+    #[test]
+    fn can_mint_by_minter() {
+        let mut deps = mock_dependencies(CANONICAL_LENGTH, &[]);
+        let minter = HumanAddr::from("minter");
+        let recipient = HumanAddr::from("recipient");
+        let amount = Uint128(222_222_222);
+        do_init_and_mint(&mut deps, &minter, &recipient, amount);
+    }
+
+    #[test]
+    fn others_cannot_mint() {
+        let mut deps = mock_dependencies(CANONICAL_LENGTH, &[]);
+        let minter = HumanAddr::from("minter");
+        let recipient = HumanAddr::from("recipient");
+        do_init(&mut deps, &minter);
+
+        let amount = Uint128(222_222_222);
+        let msg = HandleMsg::Mint {
+            recipient: recipient.clone(),
+            amount,
+        };
+
+        let other_address = HumanAddr::from("other");
+        let env = mock_env(&other_address, &[]);
+        let res = handle(&mut deps, env, msg);
+        assert_eq!(
+            format!("{}", res.unwrap_err()),
+            format!("{}", crate::error::ContractError::Unauthorized {})
+        );
+    }
+
+    #[test]
+    fn transfer_balance_success() {
+        let mut deps = mock_dependencies(CANONICAL_LENGTH, &[]);
+        let minter = HumanAddr::from("minter");
+        let owner = HumanAddr::from("owner");
+        let amount_initial = Uint128(222_222_222);
+        do_init_and_mint(&mut deps, &minter, &owner, amount_initial);
+
+        // Transfer
+        let recipient = HumanAddr::from("recipient");
+        let amount_transfer = Uint128(222_222);
+        let msg = HandleMsg::Transfer {
+            recipient: recipient.clone(),
+            amount: amount_transfer,
+        };
+
+        let env = mock_env(&owner, &[]);
+        let res = handle(&mut deps, env, msg.clone()).unwrap();
+        assert_eq!(0, res.messages.len());
+        assert_eq!(get_balance(&deps, owner), Uint128(222_000_000));
+        assert_eq!(get_balance(&deps, recipient), amount_transfer);
+    }
+
+    #[test]
+    fn transfer_balance_not_enough() {
+        let mut deps = mock_dependencies(CANONICAL_LENGTH, &[]);
+        let minter = HumanAddr::from("minter");
+        let owner = HumanAddr::from("owner");
+        let amount_initial = Uint128(222_221);
+        do_init_and_mint(&mut deps, &minter, &owner, amount_initial);
+
+        // Transfer
+        let recipient = HumanAddr::from("recipient");
+        let amount_transfer = Uint128(222_222);
+        let msg = HandleMsg::Transfer {
+            recipient: recipient.clone(),
+            amount: amount_transfer,
+        };
+
+        let env = mock_env(&owner, &[]);
+        let _ = handle(&mut deps, env, msg.clone()).unwrap_err(); // Will panic if no error
+    }
+}

+ 27 - 0
terra/contracts-5/cw20-wrapped/src/error.rs

@@ -0,0 +1,27 @@
+use cosmwasm_std::StdError;
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum ContractError {
+    // CW20 errors
+    #[error("{0}")]
+    Std(#[from] StdError),
+
+    #[error("Unauthorized")]
+    Unauthorized {},
+
+    #[error("Cannot set to own account")]
+    CannotSetOwnAccount {},
+
+    #[error("Invalid zero amount")]
+    InvalidZeroAmount {},
+
+    #[error("Allowance is expired")]
+    Expired {},
+
+    #[error("No allowance for this account")]
+    NoAllowance {},
+
+    #[error("Minting cannot exceed the cap")]
+    CannotExceedCap {},
+}

+ 9 - 0
terra/contracts-5/cw20-wrapped/src/lib.rs

@@ -0,0 +1,9 @@
+pub mod contract;
+mod error;
+pub mod msg;
+pub mod state;
+
+pub use crate::error::ContractError;
+
+#[cfg(all(target_arch = "wasm32", not(feature = "library")))]
+cosmwasm_std::create_entry_points!(contract);

+ 120 - 0
terra/contracts-5/cw20-wrapped/src/msg.rs

@@ -0,0 +1,120 @@
+#![allow(clippy::field_reassign_with_default)]
+use schemars::JsonSchema;
+use serde::{
+    Deserialize,
+    Serialize,
+};
+
+use cosmwasm_std::{
+    Binary,
+    HumanAddr,
+    Uint128,
+};
+use cw20::Expiration;
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct InitMsg {
+    pub name: String,
+    pub symbol: String,
+    pub asset_chain: u16,
+    pub asset_address: Binary,
+    pub decimals: u8,
+    pub mint: Option<InitMint>,
+    pub init_hook: Option<InitHook>,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct InitHook {
+    pub msg: Binary,
+    pub contract_addr: HumanAddr,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct InitMint {
+    pub recipient: HumanAddr,
+    pub amount: Uint128,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum HandleMsg {
+    /// Implements CW20. Transfer is a base message to move tokens to another account without triggering actions
+    Transfer {
+        recipient: HumanAddr,
+        amount: Uint128,
+    },
+    /// Slightly different than CW20. Burn is a base message to destroy tokens forever
+    Burn { account: HumanAddr, amount: Uint128 },
+    /// Implements CW20. Send is a base message to transfer tokens to a contract and trigger an action
+    /// on the receiving contract.
+    Send {
+        contract: HumanAddr,
+        amount: Uint128,
+        msg: Option<Binary>,
+    },
+    /// Implements CW20 "mintable" extension. If authorized, creates amount new tokens
+    /// and adds to the recipient balance.
+    Mint {
+        recipient: HumanAddr,
+        amount: Uint128,
+    },
+    /// Implements CW20 "approval" extension. Allows spender to access an additional amount tokens
+    /// from the owner's (env.sender) account. If expires is Some(), overwrites current allowance
+    /// expiration with this one.
+    IncreaseAllowance {
+        spender: HumanAddr,
+        amount: Uint128,
+        expires: Option<Expiration>,
+    },
+    /// Implements CW20 "approval" extension. Lowers the spender's access of tokens
+    /// from the owner's (env.sender) account by amount. If expires is Some(), overwrites current
+    /// allowance expiration with this one.
+    DecreaseAllowance {
+        spender: HumanAddr,
+        amount: Uint128,
+        expires: Option<Expiration>,
+    },
+    /// Implements CW20 "approval" extension. Transfers amount tokens from owner -> recipient
+    /// if `env.sender` has sufficient pre-approval.
+    TransferFrom {
+        owner: HumanAddr,
+        recipient: HumanAddr,
+        amount: Uint128,
+    },
+    /// Implements CW20 "approval" extension. Sends amount tokens from owner -> contract
+    /// if `env.sender` has sufficient pre-approval.
+    SendFrom {
+        owner: HumanAddr,
+        contract: HumanAddr,
+        amount: Uint128,
+        msg: Option<Binary>,
+    },
+    /// Implements CW20 "approval" extension. Destroys tokens forever
+    BurnFrom { owner: HumanAddr, amount: Uint128 },
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum QueryMsg {
+    // Generic information about the wrapped asset
+    WrappedAssetInfo {},
+    /// Implements CW20. Returns the current balance of the given address, 0 if unset.
+    Balance {
+        address: HumanAddr,
+    },
+    /// Implements CW20. Returns metadata on the contract - name, decimals, supply, etc.
+    TokenInfo {},
+    /// Implements CW20 "allowance" extension.
+    /// Returns how much spender can use from owner account, 0 if unset.
+    Allowance {
+        owner: HumanAddr,
+        spender: HumanAddr,
+    },
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct WrappedAssetInfoResponse {
+    pub asset_chain: u16,      // Asset chain id
+    pub asset_address: Binary, // Asset smart contract address in the original chain
+    pub bridge: HumanAddr,     // Bridge address, authorized to mint and burn wrapped tokens
+}

+ 38 - 0
terra/contracts-5/cw20-wrapped/src/state.rs

@@ -0,0 +1,38 @@
+use schemars::JsonSchema;
+use serde::{
+    Deserialize,
+    Serialize,
+};
+
+use cosmwasm_std::{
+    Binary,
+    CanonicalAddr,
+    ReadonlyStorage,
+    Storage,
+};
+use cosmwasm_storage::{
+    singleton,
+    singleton_read,
+    ReadonlySingleton,
+    Singleton,
+};
+
+pub const KEY_WRAPPED_ASSET: &[u8] = b"wrappedAsset";
+
+// Created at initialization and reference original asset and bridge address
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct WrappedAssetInfo {
+    pub asset_chain: u16,      // Asset chain id
+    pub asset_address: Binary, // Asset smart contract address on the original chain
+    pub bridge: CanonicalAddr, // Bridge address, authorized to mint and burn wrapped tokens
+}
+
+pub fn wrapped_asset_info<S: Storage>(storage: &mut S) -> Singleton<S, WrappedAssetInfo> {
+    singleton(storage, KEY_WRAPPED_ASSET)
+}
+
+pub fn wrapped_asset_info_read<S: ReadonlyStorage>(
+    storage: &S,
+) -> ReadonlySingleton<S, WrappedAssetInfo> {
+    singleton_read(storage, KEY_WRAPPED_ASSET)
+}

+ 253 - 0
terra/contracts-5/cw20-wrapped/tests/integration.rs

@@ -0,0 +1,253 @@
+static WASM: &[u8] =
+    include_bytes!("../../../target/wasm32-unknown-unknown/release/cw20_wrapped.wasm");
+
+use cosmwasm_std::{
+    from_slice,
+    Binary,
+    Env,
+    HandleResponse,
+    HandleResult,
+    HumanAddr,
+    InitResponse,
+    Uint128,
+};
+use cosmwasm_storage::to_length_prefixed;
+use cosmwasm_vm::{
+    testing::{
+        handle,
+        init,
+        mock_env,
+        mock_instance,
+        query,
+        MockApi,
+        MockQuerier,
+        MockStorage,
+    },
+    Api,
+    Instance,
+    Storage,
+};
+use cw20_wrapped::{
+    msg::{
+        HandleMsg,
+        InitMsg,
+        QueryMsg,
+    },
+    state::{
+        WrappedAssetInfo,
+        KEY_WRAPPED_ASSET,
+    },
+    ContractError,
+};
+
+enum TestAddress {
+    INITIALIZER,
+    RECIPIENT,
+    SENDER,
+}
+
+impl TestAddress {
+    fn value(&self) -> HumanAddr {
+        match self {
+            TestAddress::INITIALIZER => HumanAddr::from("addr0000"),
+            TestAddress::RECIPIENT => HumanAddr::from("addr2222"),
+            TestAddress::SENDER => HumanAddr::from("addr3333"),
+        }
+    }
+}
+
+fn mock_env_height(signer: &HumanAddr, height: u64, time: u64) -> Env {
+    let mut env = mock_env(signer, &[]);
+    env.block.height = height;
+    env.block.time = time;
+    env
+}
+
+fn get_wrapped_asset_info<S: Storage>(storage: &S) -> WrappedAssetInfo {
+    let key = to_length_prefixed(KEY_WRAPPED_ASSET);
+    let data = storage
+        .get(&key)
+        .0
+        .expect("error getting data")
+        .expect("data should exist");
+    from_slice(&data).expect("invalid data")
+}
+
+fn do_init(height: u64) -> Instance<MockStorage, MockApi, MockQuerier> {
+    let mut deps = mock_instance(WASM, &[]);
+    let init_msg = InitMsg {
+        asset_chain: 1,
+        asset_address: vec![1; 32].into(),
+        decimals: 10,
+        mint: None,
+        init_hook: None,
+    };
+    let env = mock_env_height(&TestAddress::INITIALIZER.value(), height, 0);
+    let res: InitResponse = init(&mut deps, env, init_msg).unwrap();
+    assert_eq!(0, res.messages.len());
+
+    // query the store directly
+    let api = deps.api;
+    deps.with_storage(|storage| {
+        assert_eq!(
+            get_wrapped_asset_info(storage),
+            WrappedAssetInfo {
+                asset_chain: 1,
+                asset_address: vec![1; 32].into(),
+                bridge: api.canonical_address(&TestAddress::INITIALIZER.value()).0?,
+            }
+        );
+        Ok(())
+    })
+    .unwrap();
+    deps
+}
+
+fn do_mint(
+    deps: &mut Instance<MockStorage, MockApi, MockQuerier>,
+    height: u64,
+    recipient: &HumanAddr,
+    amount: &Uint128,
+) {
+    let mint_msg = HandleMsg::Mint {
+        recipient: recipient.clone(),
+        amount: amount.clone(),
+    };
+    let env = mock_env_height(&TestAddress::INITIALIZER.value(), height, 0);
+    let handle_response: HandleResponse = handle(deps, env, mint_msg).unwrap();
+    assert_eq!(0, handle_response.messages.len());
+}
+
+fn do_transfer(
+    deps: &mut Instance<MockStorage, MockApi, MockQuerier>,
+    height: u64,
+    sender: &HumanAddr,
+    recipient: &HumanAddr,
+    amount: &Uint128,
+) {
+    let transfer_msg = HandleMsg::Transfer {
+        recipient: recipient.clone(),
+        amount: amount.clone(),
+    };
+    let env = mock_env_height(sender, height, 0);
+    let handle_response: HandleResponse = handle(deps, env, transfer_msg).unwrap();
+    assert_eq!(0, handle_response.messages.len());
+}
+
+fn check_balance(
+    deps: &mut Instance<MockStorage, MockApi, MockQuerier>,
+    address: &HumanAddr,
+    amount: &Uint128,
+) {
+    let query_response = query(
+        deps,
+        QueryMsg::Balance {
+            address: address.clone(),
+        },
+    )
+    .unwrap();
+    assert_eq!(
+        query_response.as_slice(),
+        format!("{{\"balance\":\"{}\"}}", amount.u128()).as_bytes()
+    );
+}
+
+fn check_token_details(deps: &mut Instance<MockStorage, MockApi, MockQuerier>, supply: &Uint128) {
+    let query_response = query(deps, QueryMsg::TokenInfo {}).unwrap();
+    assert_eq!(
+        query_response.as_slice(),
+        format!(
+            "{{\"name\":\"Wormhole Wrapped\",\
+        \"symbol\":\"WWT\",\
+        \"decimals\":10,\
+        \"total_supply\":\"{}\"}}",
+            supply.u128()
+        )
+        .as_bytes()
+    );
+}
+
+#[test]
+fn init_works() {
+    let mut deps = do_init(111);
+    check_token_details(&mut deps, &Uint128(0));
+}
+
+#[test]
+fn query_works() {
+    let mut deps = do_init(111);
+
+    let query_response = query(&mut deps, QueryMsg::WrappedAssetInfo {}).unwrap();
+    assert_eq!(
+        query_response.as_slice(),
+        format!(
+            "{{\"asset_chain\":1,\
+        \"asset_address\":\"{}\",\
+        \"bridge\":\"{}\"}}",
+            Binary::from(vec![1; 32]).to_base64(),
+            TestAddress::INITIALIZER.value().as_str()
+        )
+        .as_bytes()
+    );
+}
+
+#[test]
+fn mint_works() {
+    let mut deps = do_init(111);
+
+    do_mint(
+        &mut deps,
+        112,
+        &TestAddress::RECIPIENT.value(),
+        &Uint128(123_123_123),
+    );
+
+    check_balance(
+        &mut deps,
+        &TestAddress::RECIPIENT.value(),
+        &Uint128(123_123_123),
+    );
+    check_token_details(&mut deps, &Uint128(123_123_123));
+}
+
+#[test]
+fn others_cannot_mint() {
+    let mut deps = do_init(111);
+
+    let mint_msg = HandleMsg::Mint {
+        recipient: TestAddress::RECIPIENT.value(),
+        amount: Uint128(123_123_123),
+    };
+    let env = mock_env_height(&TestAddress::RECIPIENT.value(), 112, 0);
+    let handle_result: HandleResult<HandleResponse> = handle(&mut deps, env, mint_msg);
+    assert_eq!(
+        format!("{}", handle_result.unwrap_err()),
+        format!("{}", ContractError::Unauthorized {})
+    );
+}
+
+#[test]
+fn transfer_works() {
+    let mut deps = do_init(111);
+
+    do_mint(
+        &mut deps,
+        112,
+        &TestAddress::SENDER.value(),
+        &Uint128(123_123_123),
+    );
+    do_transfer(
+        &mut deps,
+        113,
+        &TestAddress::SENDER.value(),
+        &TestAddress::RECIPIENT.value(),
+        &Uint128(123_123_000),
+    );
+
+    check_balance(&mut deps, &TestAddress::SENDER.value(), &Uint128(123));
+    check_balance(
+        &mut deps,
+        &TestAddress::RECIPIENT.value(),
+        &Uint128(123_123_000),
+    );
+}

+ 5 - 0
terra/contracts-5/token-bridge/.cargo/config

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

+ 37 - 0
terra/contracts-5/token-bridge/Cargo.toml

@@ -0,0 +1,37 @@
+[package]
+name = "token-bridge"
+version = "0.1.0"
+authors = ["Yuriy Savchenko <yuriy.savchenko@gmail.com>"]
+edition = "2018"
+description = "Wormhole token bridge"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[features]
+backtraces = ["cosmwasm-std/backtraces"]
+# use library feature to disable all init/handle/query exports
+library = []
+
+[dependencies]
+cosmwasm-std = { version = "0.10.0" }
+cosmwasm-storage = { version = "0.10.0" }
+schemars = "0.7"
+serde = { version = "1.0.103", default-features = false, features = ["derive"] }
+cw20 = "0.2.2"
+cw20-base = { version = "0.2.2", features = ["library"] }
+cw20-wrapped = { path = "../cw20-wrapped", features = ["library"] }
+terraswap = "1.1.0"
+wormhole = { path = "../wormhole", features = ["library"] }
+
+thiserror = { version = "1.0.20" }
+k256 = { version = "0.5.9", default-features = false, features = ["ecdsa"] }
+sha3 = { version = "0.9.1", default-features = false }
+generic-array = { version = "0.14.4" }
+hex = "0.4.2"
+lazy_static = "1.4.0"
+bigint = "4"
+
+[dev-dependencies]
+cosmwasm-vm = { version = "0.10.0", default-features = false, features = ["default-cranelift"] }
+serde_json = "1.0"

+ 777 - 0
terra/contracts-5/token-bridge/src/contract.rs

@@ -0,0 +1,777 @@
+use crate::msg::WrappedRegistryResponse;
+use cosmwasm_std::{
+    log,
+    to_binary,
+    Api,
+    Binary,
+    CanonicalAddr,
+    Coin,
+    CosmosMsg,
+    Env,
+    Extern,
+    HandleResponse,
+    HumanAddr,
+    InitResponse,
+    Querier,
+    QueryRequest,
+    StdError,
+    StdResult,
+    Storage,
+    Uint128,
+    WasmMsg,
+    WasmQuery,
+};
+
+use crate::{
+    msg::{
+        HandleMsg,
+        InitMsg,
+        QueryMsg,
+    },
+    state::{
+        bridge_contracts,
+        bridge_contracts_read,
+        config,
+        config_read,
+        receive_native,
+        send_native,
+        wrapped_asset,
+        wrapped_asset_address,
+        wrapped_asset_address_read,
+        wrapped_asset_read,
+        Action,
+        AssetMeta,
+        ConfigInfo,
+        RegisterChain,
+        TokenBridgeMessage,
+        TransferInfo,
+    },
+};
+use wormhole::{
+    byte_utils::{
+        extend_address_to_32,
+        extend_string_to_32,
+        get_string_from_32,
+        ByteUtils,
+    },
+    error::ContractError,
+};
+
+use cw20_base::msg::{
+    HandleMsg as TokenMsg,
+    QueryMsg as TokenQuery,
+};
+
+use wormhole::msg::{
+    HandleMsg as WormholeHandleMsg,
+    QueryMsg as WormholeQueryMsg,
+};
+
+use wormhole::state::{
+    vaa_archive_add,
+    vaa_archive_check,
+    GovernancePacket,
+    ParsedVAA,
+};
+
+use cw20::TokenInfoResponse;
+
+use cw20_wrapped::msg::{
+    HandleMsg as WrappedMsg,
+    InitHook,
+    InitMsg as WrappedInit,
+    QueryMsg as WrappedQuery,
+    WrappedAssetInfoResponse,
+};
+use terraswap::asset::{
+    Asset,
+    AssetInfo,
+};
+
+use sha3::{
+    Digest,
+    Keccak256,
+};
+use std::cmp::{
+    max,
+    min,
+};
+
+// Chain ID of Terra
+const CHAIN_ID: u16 = 3;
+
+const WRAPPED_ASSET_UPDATING: &str = "updating";
+
+pub fn init<S: Storage, A: Api, Q: Querier>(
+    deps: &mut Extern<S, A, Q>,
+    _env: Env,
+    msg: InitMsg,
+) -> StdResult<InitResponse> {
+    // Save general wormhole info
+    let state = ConfigInfo {
+        gov_chain: msg.gov_chain,
+        gov_address: msg.gov_address.as_slice().to_vec(),
+        wormhole_contract: msg.wormhole_contract,
+        wrapped_asset_code_id: msg.wrapped_asset_code_id,
+    };
+    config(&mut deps.storage).save(&state)?;
+
+    Ok(InitResponse::default())
+}
+
+pub fn coins_after_tax<S: Storage, A: Api, Q: Querier>(
+    deps: &mut Extern<S, A, Q>,
+    coins: Vec<Coin>,
+) -> StdResult<Vec<Coin>> {
+    let mut res = vec![];
+    for coin in coins {
+        let asset = Asset {
+            amount: coin.amount.clone(),
+            info: AssetInfo::NativeToken {
+                denom: coin.denom.clone(),
+            },
+        };
+        res.push(asset.deduct_tax(&deps)?);
+    }
+    Ok(res)
+}
+
+pub fn parse_vaa<S: Storage, A: Api, Q: Querier>(
+    deps: &mut Extern<S, A, Q>,
+    block_time: u64,
+    data: &Binary,
+) -> StdResult<ParsedVAA> {
+    let cfg = config_read(&deps.storage).load()?;
+    let vaa: ParsedVAA = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
+        contract_addr: cfg.wormhole_contract.clone(),
+        msg: to_binary(&WormholeQueryMsg::VerifyVAA {
+            vaa: data.clone(),
+            block_time,
+        })?,
+    }))?;
+    Ok(vaa)
+}
+
+pub fn handle<S: Storage, A: Api, Q: Querier>(
+    deps: &mut Extern<S, A, Q>,
+    env: Env,
+    msg: HandleMsg,
+) -> StdResult<HandleResponse> {
+    match msg {
+        HandleMsg::RegisterAssetHook { asset_id } => {
+            handle_register_asset(deps, env, &asset_id.as_slice())
+        }
+        HandleMsg::InitiateTransfer {
+            asset,
+            amount,
+            recipient_chain,
+            recipient,
+            fee,
+            nonce,
+        } => handle_initiate_transfer(
+            deps,
+            env,
+            asset,
+            amount,
+            recipient_chain,
+            recipient.as_slice().to_vec(),
+            fee,
+            nonce,
+        ),
+        HandleMsg::SubmitVaa { data } => submit_vaa(deps, env, &data),
+        HandleMsg::CreateAssetMeta {
+            asset_address,
+            nonce,
+        } => handle_create_asset_meta(deps, env, &asset_address, nonce),
+    }
+}
+
+/// Handle wrapped asset registration messages
+fn handle_register_asset<S: Storage, A: Api, Q: Querier>(
+    deps: &mut Extern<S, A, Q>,
+    env: Env,
+    asset_id: &[u8],
+) -> StdResult<HandleResponse> {
+    let mut bucket = wrapped_asset(&mut deps.storage);
+    let result = bucket.load(asset_id);
+    let result = result.map_err(|_| ContractError::RegistrationForbidden.std())?;
+    if result != HumanAddr::from(WRAPPED_ASSET_UPDATING) {
+        return ContractError::AssetAlreadyRegistered.std_err();
+    }
+
+    bucket.save(asset_id, &env.message.sender)?;
+
+    let contract_address: CanonicalAddr = deps.api.canonical_address(&env.message.sender)?;
+    wrapped_asset_address(&mut deps.storage)
+        .save(contract_address.as_slice(), &asset_id.to_vec())?;
+
+    Ok(HandleResponse {
+        messages: vec![],
+        log: vec![
+            log("action", "register_asset"),
+            log("asset_id", format!("{:?}", asset_id)),
+            log("contract_addr", env.message.sender),
+        ],
+        data: None,
+    })
+}
+
+fn handle_attest_meta<S: Storage, A: Api, Q: Querier>(
+    deps: &mut Extern<S, A, Q>,
+    env: Env,
+    emitter_chain: u16,
+    emitter_address: Vec<u8>,
+    data: &Vec<u8>,
+) -> StdResult<HandleResponse> {
+    let meta = AssetMeta::deserialize(data)?;
+
+    let expected_contract =
+        bridge_contracts_read(&deps.storage).load(&emitter_chain.to_be_bytes())?;
+
+    // must be sent by a registered token bridge contract
+    if expected_contract != emitter_address {
+        return Err(StdError::unauthorized());
+    }
+
+    if CHAIN_ID == meta.token_chain {
+        return Err(StdError::generic_err(
+            "this asset is native to this chain and should not be attested",
+        ));
+    }
+
+    let cfg = config_read(&deps.storage).load()?;
+    let asset_id = build_asset_id(meta.token_chain, &meta.token_address.as_slice());
+
+    if wrapped_asset_read(&mut deps.storage)
+        .load(&asset_id)
+        .is_ok()
+    {
+        return Err(StdError::generic_err(
+            "this asset has already been attested",
+        ));
+    }
+
+    wrapped_asset(&mut deps.storage).save(&asset_id, &HumanAddr::from(WRAPPED_ASSET_UPDATING))?;
+
+    Ok(HandleResponse {
+        messages: vec![CosmosMsg::Wasm(WasmMsg::Instantiate {
+            code_id: cfg.wrapped_asset_code_id,
+            msg: to_binary(&WrappedInit {
+                name: get_string_from_32(&meta.name)?,
+                symbol: get_string_from_32(&meta.symbol)?,
+                asset_chain: meta.token_chain,
+                asset_address: meta.token_address.to_vec().into(),
+                decimals: min(meta.decimals, 8u8),
+                mint: None,
+                init_hook: Some(InitHook {
+                    contract_addr: env.contract.address,
+                    msg: to_binary(&HandleMsg::RegisterAssetHook {
+                        asset_id: asset_id.to_vec().into(),
+                    })?,
+                }),
+            })?,
+            send: vec![],
+            label: None,
+        })],
+        log: vec![],
+        data: None,
+    })
+}
+
+fn handle_create_asset_meta<S: Storage, A: Api, Q: Querier>(
+    deps: &mut Extern<S, A, Q>,
+    env: Env,
+    asset_address: &HumanAddr,
+    nonce: u32,
+) -> StdResult<HandleResponse> {
+    let cfg = config_read(&deps.storage).load()?;
+
+    let request = QueryRequest::Wasm(WasmQuery::Smart {
+        contract_addr: asset_address.clone(),
+        msg: to_binary(&TokenQuery::TokenInfo {})?,
+    });
+
+    let asset_canonical = deps.api.canonical_address(asset_address)?;
+    let token_info: TokenInfoResponse = deps.querier.query(&request)?;
+
+    let meta: AssetMeta = AssetMeta {
+        token_chain: CHAIN_ID,
+        token_address: extend_address_to_32(&asset_canonical),
+        decimals: token_info.decimals,
+        symbol: extend_string_to_32(&token_info.symbol)?,
+        name: extend_string_to_32(&token_info.name)?,
+    };
+
+    let token_bridge_message = TokenBridgeMessage {
+        action: Action::ATTEST_META,
+        payload: meta.serialize().to_vec(),
+    };
+
+    Ok(HandleResponse {
+        messages: vec![CosmosMsg::Wasm(WasmMsg::Execute {
+            contract_addr: cfg.wormhole_contract,
+            msg: to_binary(&WormholeHandleMsg::PostMessage {
+                message: Binary::from(token_bridge_message.serialize()),
+                nonce,
+            })?,
+            // forward coins sent to this message
+            send: coins_after_tax(deps, env.message.sent_funds.clone())?,
+        })],
+        log: vec![
+            log("meta.token_chain", CHAIN_ID),
+            log("meta.token", asset_address),
+            log("meta.nonce", nonce),
+            log("meta.block_time", env.block.time),
+        ],
+        data: None,
+    })
+}
+
+fn submit_vaa<S: Storage, A: Api, Q: Querier>(
+    deps: &mut Extern<S, A, Q>,
+    env: Env,
+    data: &Binary,
+) -> StdResult<HandleResponse> {
+    let state = config_read(&deps.storage).load()?;
+
+    let vaa = parse_vaa(deps, env.block.time, data)?;
+    let data = vaa.payload;
+
+    if vaa_archive_check(&deps.storage, vaa.hash.as_slice()) {
+        return ContractError::VaaAlreadyExecuted.std_err();
+    }
+    vaa_archive_add(&mut deps.storage, vaa.hash.as_slice())?;
+
+    // check if vaa is from governance
+    if state.gov_chain == vaa.emitter_chain && state.gov_address == vaa.emitter_address {
+        return handle_governance_payload(deps, env, &data);
+    }
+
+    let message = TokenBridgeMessage::deserialize(&data)?;
+
+    let result = match message.action {
+        Action::TRANSFER => handle_complete_transfer(
+            deps,
+            env,
+            vaa.emitter_chain,
+            vaa.emitter_address,
+            &message.payload,
+        ),
+        Action::ATTEST_META => handle_attest_meta(
+            deps,
+            env,
+            vaa.emitter_chain,
+            vaa.emitter_address,
+            &message.payload,
+        ),
+        _ => ContractError::InvalidVAAAction.std_err(),
+    };
+    return result;
+}
+
+fn handle_governance_payload<S: Storage, A: Api, Q: Querier>(
+    deps: &mut Extern<S, A, Q>,
+    env: Env,
+    data: &Vec<u8>,
+) -> StdResult<HandleResponse> {
+    let gov_packet = GovernancePacket::deserialize(&data)?;
+    let module = get_string_from_32(&gov_packet.module)?;
+
+    if module != "TokenBridge" {
+        return Err(StdError::generic_err("this is not a valid module"));
+    }
+
+    if gov_packet.chain != 0 && gov_packet.chain != CHAIN_ID {
+        return Err(StdError::generic_err(
+            "the governance VAA is for another chain",
+        ));
+    }
+
+    match gov_packet.action {
+        1u8 => handle_register_chain(deps, env, &gov_packet.payload),
+        _ => ContractError::InvalidVAAAction.std_err(),
+    }
+}
+
+fn handle_register_chain<S: Storage, A: Api, Q: Querier>(
+    deps: &mut Extern<S, A, Q>,
+    env: Env,
+    data: &Vec<u8>,
+) -> StdResult<HandleResponse> {
+    let RegisterChain {
+        chain_id,
+        chain_address,
+    } = RegisterChain::deserialize(&data)?;
+
+    let existing = bridge_contracts_read(&deps.storage).load(&chain_id.to_be_bytes());
+    if existing.is_ok() {
+        return Err(StdError::generic_err(
+            "bridge contract already exists for this chain",
+        ));
+    }
+
+    let mut bucket = bridge_contracts(&mut deps.storage);
+    bucket.save(&chain_id.to_be_bytes(), &chain_address)?;
+
+    Ok(HandleResponse {
+        messages: vec![],
+        log: vec![
+            log("chain_id", chain_id),
+            log("chain_address", hex::encode(chain_address)),
+        ],
+        data: None,
+    })
+}
+
+fn handle_complete_transfer<S: Storage, A: Api, Q: Querier>(
+    deps: &mut Extern<S, A, Q>,
+    env: Env,
+    emitter_chain: u16,
+    emitter_address: Vec<u8>,
+    data: &Vec<u8>,
+) -> StdResult<HandleResponse> {
+    let transfer_info = TransferInfo::deserialize(&data)?;
+
+    let expected_contract =
+        bridge_contracts_read(&deps.storage).load(&emitter_chain.to_be_bytes())?;
+
+    // must be sent by a registered token bridge contract
+    if expected_contract != emitter_address {
+        return Err(StdError::unauthorized());
+    }
+
+    if transfer_info.recipient_chain != CHAIN_ID {
+        return Err(StdError::generic_err(
+            "this transfer is not directed at this chain",
+        ));
+    }
+
+    let token_chain = transfer_info.token_chain;
+    let target_address = (&transfer_info.recipient.as_slice()).get_address(0);
+
+    let (not_supported_amount, mut amount) = transfer_info.amount;
+    let (not_supported_fee, mut fee) = transfer_info.fee;
+
+    amount = amount.checked_sub(fee).unwrap();
+
+    // Check high 128 bit of amount value to be empty
+    if not_supported_amount != 0 || not_supported_fee != 0 {
+        return ContractError::AmountTooHigh.std_err();
+    }
+
+    if token_chain != CHAIN_ID {
+        let asset_address = transfer_info.token_address;
+        let asset_id = build_asset_id(token_chain, &asset_address);
+
+        // Check if this asset is already deployed
+        let contract_addr = wrapped_asset_read(&deps.storage).load(&asset_id).ok();
+
+        return if let Some(contract_addr) = contract_addr {
+            // Asset already deployed, just mint
+
+            let recipient = deps
+                .api
+                .human_address(&target_address)
+                .or_else(|_| ContractError::WrongTargetAddressFormat.std_err())?;
+
+            let mut messages = vec![CosmosMsg::Wasm(WasmMsg::Execute {
+                contract_addr: contract_addr.clone(),
+                msg: to_binary(&WrappedMsg::Mint {
+                    recipient: recipient.clone(),
+                    amount: Uint128::from(amount),
+                })?,
+                send: vec![],
+            })];
+            if fee != 0 {
+                messages.push(CosmosMsg::Wasm(WasmMsg::Execute {
+                    contract_addr: contract_addr.clone(),
+                    msg: to_binary(&WrappedMsg::Mint {
+                        recipient: env.message.sender.clone(),
+                        amount: Uint128::from(fee),
+                    })?,
+                    send: vec![],
+                }))
+            }
+
+            Ok(HandleResponse {
+                messages,
+                log: vec![
+                    log("action", "complete_transfer_wrapped"),
+                    log("contract", contract_addr),
+                    log("recipient", recipient),
+                    log("amount", amount),
+                ],
+                data: None,
+            })
+        } else {
+            Err(StdError::generic_err("Wrapped asset not deployed. To deploy, invoke CreateWrapped with the associated AssetMeta"))
+        };
+    } else {
+        let token_address = transfer_info.token_address.as_slice().get_address(0);
+
+        let recipient = deps.api.human_address(&target_address)?;
+        let contract_addr = deps.api.human_address(&token_address)?;
+
+        // note -- here the amount is the amount the recipient will receive;
+        // amount + fee is the total sent
+        receive_native(&mut deps.storage, &token_address, Uint128(amount + fee))?;
+
+        // undo normalization to 8 decimals
+        let token_info: TokenInfoResponse =
+            deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
+                contract_addr: contract_addr.clone(),
+                msg: to_binary(&TokenQuery::TokenInfo {})?,
+            }))?;
+
+        let decimals = token_info.decimals;
+        let multiplier = 10u128.pow((max(decimals, 8u8) - 8u8) as u32);
+        amount = amount.checked_mul(multiplier).unwrap();
+        fee = fee.checked_mul(multiplier).unwrap();
+
+        let mut messages = vec![CosmosMsg::Wasm(WasmMsg::Execute {
+            contract_addr: contract_addr.clone(),
+            msg: to_binary(&TokenMsg::Transfer {
+                recipient: recipient.clone(),
+                amount: Uint128::from(amount),
+            })?,
+            send: vec![],
+        })];
+
+        if fee != 0 {
+            messages.push(CosmosMsg::Wasm(WasmMsg::Execute {
+                contract_addr: contract_addr.clone(),
+                msg: to_binary(&TokenMsg::Transfer {
+                    recipient: env.message.sender.clone(),
+                    amount: Uint128::from(fee),
+                })?,
+                send: vec![],
+            }))
+        }
+
+        Ok(HandleResponse {
+            messages,
+            log: vec![
+                log("action", "complete_transfer_native"),
+                log("recipient", recipient),
+                log("contract", contract_addr),
+                log("amount", amount),
+            ],
+            data: None,
+        })
+    }
+}
+
+fn handle_initiate_transfer<S: Storage, A: Api, Q: Querier>(
+    deps: &mut Extern<S, A, Q>,
+    env: Env,
+    asset: HumanAddr,
+    mut amount: Uint128,
+    recipient_chain: u16,
+    recipient: Vec<u8>,
+    mut fee: Uint128,
+    nonce: u32,
+) -> StdResult<HandleResponse> {
+    if recipient_chain == CHAIN_ID {
+        return ContractError::SameSourceAndTarget.std_err();
+    }
+
+    if amount.is_zero() {
+        return ContractError::AmountTooLow.std_err();
+    }
+
+    if fee > amount {
+        return Err(StdError::generic_err("fee greater than sent amount"));
+    }
+
+    let asset_chain: u16;
+    let asset_address: Vec<u8>;
+
+    let cfg: ConfigInfo = config_read(&deps.storage).load()?;
+    let asset_canonical: CanonicalAddr = deps.api.canonical_address(&asset)?;
+
+    let mut messages: Vec<CosmosMsg> = vec![];
+
+    match wrapped_asset_address_read(&deps.storage).load(asset_canonical.as_slice()) {
+        Ok(_) => {
+            // This is a deployed wrapped asset, burn it
+            messages.push(CosmosMsg::Wasm(WasmMsg::Execute {
+                contract_addr: asset.clone(),
+                msg: to_binary(&WrappedMsg::Burn {
+                    account: env.message.sender.clone(),
+                    amount,
+                })?,
+                send: vec![],
+            }));
+            let request = QueryRequest::<()>::Wasm(WasmQuery::Smart {
+                contract_addr: asset,
+                msg: to_binary(&WrappedQuery::WrappedAssetInfo {})?,
+            });
+            let wrapped_token_info: WrappedAssetInfoResponse =
+                deps.querier.custom_query(&request)?;
+            asset_chain = wrapped_token_info.asset_chain;
+            asset_address = wrapped_token_info.asset_address.as_slice().to_vec();
+        }
+        Err(_) => {
+            // normalize amount to 8 decimals when it sent over the wormhole
+            let token_info: TokenInfoResponse =
+                deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
+                    contract_addr: asset.clone(),
+                    msg: to_binary(&TokenQuery::TokenInfo {})?,
+                }))?;
+
+            let decimals = token_info.decimals;
+            let multiplier = 10u128.pow((max(decimals, 8u8) - 8u8) as u32);
+            // chop off dust
+            amount = Uint128(
+                amount
+                    .u128()
+                    .checked_sub(amount.u128().checked_rem(multiplier).unwrap())
+                    .unwrap(),
+            );
+            fee = Uint128(
+                fee.u128()
+                    .checked_sub(fee.u128().checked_rem(multiplier).unwrap())
+                    .unwrap(),
+            );
+
+            // This is a regular asset, transfer its balance
+            messages.push(CosmosMsg::Wasm(WasmMsg::Execute {
+                contract_addr: asset,
+                msg: to_binary(&TokenMsg::TransferFrom {
+                    owner: env.message.sender.clone(),
+                    recipient: env.contract.address.clone(),
+                    amount,
+                })?,
+                send: vec![],
+            }));
+            asset_address = extend_address_to_32(&asset_canonical);
+            asset_chain = CHAIN_ID;
+
+            // convert to normalized amounts before recording & posting vaa
+            amount = Uint128(amount.u128().checked_div(multiplier).unwrap());
+            fee = Uint128(fee.u128().checked_div(multiplier).unwrap());
+
+            send_native(&mut deps.storage, &asset_canonical, amount)?;
+        }
+    };
+
+    let transfer_info = TransferInfo {
+        token_chain: asset_chain,
+        token_address: asset_address.clone(),
+        amount: (0, amount.u128()),
+        recipient_chain,
+        recipient: recipient.clone(),
+        fee: (0, fee.u128()),
+    };
+
+    let token_bridge_message = TokenBridgeMessage {
+        action: Action::TRANSFER,
+        payload: transfer_info.serialize(),
+    };
+
+    messages.push(CosmosMsg::Wasm(WasmMsg::Execute {
+        contract_addr: cfg.wormhole_contract,
+        msg: to_binary(&WormholeHandleMsg::PostMessage {
+            message: Binary::from(token_bridge_message.serialize()),
+            nonce,
+        })?,
+        // forward coins sent to this message
+        send: coins_after_tax(deps, env.message.sent_funds.clone())?,
+    }));
+
+    Ok(HandleResponse {
+        messages,
+        log: vec![
+            log("transfer.token_chain", asset_chain),
+            log("transfer.token", hex::encode(asset_address)),
+            log(
+                "transfer.sender",
+                hex::encode(extend_address_to_32(
+                    &deps.api.canonical_address(&env.message.sender)?,
+                )),
+            ),
+            log("transfer.recipient_chain", recipient_chain),
+            log("transfer.recipient", hex::encode(recipient)),
+            log("transfer.amount", amount),
+            log("transfer.nonce", nonce),
+            log("transfer.block_time", env.block.time),
+        ],
+        data: None,
+    })
+}
+
+pub fn query<S: Storage, A: Api, Q: Querier>(
+    deps: &Extern<S, A, Q>,
+    msg: QueryMsg,
+) -> StdResult<Binary> {
+    match msg {
+        QueryMsg::WrappedRegistry { chain, address } => {
+            to_binary(&query_wrapped_registry(deps, chain, address.as_slice())?)
+        }
+    }
+}
+
+pub fn query_wrapped_registry<S: Storage, A: Api, Q: Querier>(
+    deps: &Extern<S, A, Q>,
+    chain: u16,
+    address: &[u8],
+) -> StdResult<WrappedRegistryResponse> {
+    let asset_id = build_asset_id(chain, address);
+    // Check if this asset is already deployed
+    match wrapped_asset_read(&deps.storage).load(&asset_id) {
+        Ok(address) => Ok(WrappedRegistryResponse { address }),
+        Err(_) => ContractError::AssetNotFound.std_err(),
+    }
+}
+
+fn build_asset_id(chain: u16, address: &[u8]) -> Vec<u8> {
+    let mut asset_id: Vec<u8> = vec![];
+    asset_id.extend_from_slice(&chain.to_be_bytes());
+    asset_id.extend_from_slice(address);
+
+    let mut hasher = Keccak256::new();
+    hasher.update(asset_id);
+    hasher.finalize().to_vec()
+}
+
+#[cfg(test)]
+mod tests {
+    use cosmwasm_std::{
+        to_binary,
+        Binary,
+        StdResult,
+    };
+
+    #[test]
+    fn test_me() -> StdResult<()> {
+        let x = vec![
+            1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 96u8, 180u8, 94u8, 195u8, 0u8, 0u8,
+            0u8, 1u8, 0u8, 3u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 38u8,
+            229u8, 4u8, 215u8, 149u8, 163u8, 42u8, 54u8, 156u8, 236u8, 173u8, 168u8, 72u8, 220u8,
+            100u8, 90u8, 154u8, 159u8, 160u8, 215u8, 0u8, 91u8, 48u8, 44u8, 48u8, 44u8, 51u8, 44u8,
+            48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8,
+            48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 53u8, 55u8, 44u8, 52u8,
+            54u8, 44u8, 50u8, 53u8, 53u8, 44u8, 53u8, 48u8, 44u8, 50u8, 52u8, 51u8, 44u8, 49u8,
+            48u8, 54u8, 44u8, 49u8, 50u8, 50u8, 44u8, 49u8, 49u8, 48u8, 44u8, 49u8, 50u8, 53u8,
+            44u8, 56u8, 56u8, 44u8, 55u8, 51u8, 44u8, 49u8, 56u8, 57u8, 44u8, 50u8, 48u8, 55u8,
+            44u8, 49u8, 48u8, 52u8, 44u8, 56u8, 51u8, 44u8, 49u8, 49u8, 57u8, 44u8, 49u8, 50u8,
+            55u8, 44u8, 49u8, 57u8, 50u8, 44u8, 49u8, 52u8, 55u8, 44u8, 56u8, 57u8, 44u8, 48u8,
+            44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8,
+            44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8,
+            44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8,
+            44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8,
+            44u8, 48u8, 44u8, 51u8, 44u8, 50u8, 51u8, 50u8, 44u8, 48u8, 44u8, 51u8, 44u8, 48u8,
+            44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8,
+            44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 53u8, 51u8, 44u8, 49u8, 49u8,
+            54u8, 44u8, 52u8, 56u8, 44u8, 49u8, 49u8, 54u8, 44u8, 49u8, 52u8, 57u8, 44u8, 49u8,
+            48u8, 56u8, 44u8, 49u8, 49u8, 51u8, 44u8, 56u8, 44u8, 48u8, 44u8, 50u8, 51u8, 50u8,
+            44u8, 52u8, 57u8, 44u8, 49u8, 53u8, 50u8, 44u8, 49u8, 44u8, 50u8, 56u8, 44u8, 50u8,
+            48u8, 51u8, 44u8, 50u8, 49u8, 50u8, 44u8, 50u8, 50u8, 49u8, 44u8, 50u8, 52u8, 49u8,
+            44u8, 56u8, 53u8, 44u8, 49u8, 48u8, 57u8, 93u8,
+        ];
+        let b = Binary::from(x.clone());
+        let y = b.as_slice().to_vec();
+        assert_eq!(x, y);
+        Ok(())
+    }
+}

+ 10 - 0
terra/contracts-5/token-bridge/src/lib.rs

@@ -0,0 +1,10 @@
+#[cfg(test)]
+#[macro_use]
+extern crate lazy_static;
+
+pub mod contract;
+pub mod msg;
+pub mod state;
+
+#[cfg(all(target_arch = "wasm32", not(feature = "library")))]
+cosmwasm_std::create_entry_points!(contract);

+ 64 - 0
terra/contracts-5/token-bridge/src/msg.rs

@@ -0,0 +1,64 @@
+use cosmwasm_std::{
+    Binary,
+    HumanAddr,
+    Uint128,
+};
+use schemars::JsonSchema;
+use serde::{
+    Deserialize,
+    Serialize,
+};
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct InitMsg {
+    // governance contract details
+    pub gov_chain: u16,
+    pub gov_address: Binary,
+
+    pub wormhole_contract: HumanAddr,
+    pub wrapped_asset_code_id: u64,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum HandleMsg {
+    RegisterAssetHook {
+        asset_id: Binary,
+    },
+
+    InitiateTransfer {
+        asset: HumanAddr,
+        amount: Uint128,
+        recipient_chain: u16,
+        recipient: Binary,
+        fee: Uint128,
+        nonce: u32,
+    },
+
+    SubmitVaa {
+        data: Binary,
+    },
+
+    CreateAssetMeta {
+        asset_address: HumanAddr,
+        nonce: u32,
+    },
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum QueryMsg {
+    WrappedRegistry { chain: u16, address: Binary },
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct WrappedRegistryResponse {
+    pub address: HumanAddr,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum WormholeQueryMsg {
+    VerifyVAA { vaa: Binary, block_time: u64 },
+}

+ 247 - 0
terra/contracts-5/token-bridge/src/state.rs

@@ -0,0 +1,247 @@
+use schemars::JsonSchema;
+use serde::{
+    Deserialize,
+    Serialize,
+};
+
+use cosmwasm_std::{
+    CanonicalAddr,
+    HumanAddr,
+    StdError,
+    StdResult,
+    Storage,
+    Uint128,
+};
+use cosmwasm_storage::{
+    bucket,
+    bucket_read,
+    singleton,
+    singleton_read,
+    Bucket,
+    ReadonlyBucket,
+    ReadonlySingleton,
+    Singleton,
+};
+
+use wormhole::byte_utils::ByteUtils;
+
+pub static CONFIG_KEY: &[u8] = b"config";
+pub static WRAPPED_ASSET_KEY: &[u8] = b"wrapped_asset";
+pub static WRAPPED_ASSET_ADDRESS_KEY: &[u8] = b"wrapped_asset_address";
+pub static BRIDGE_CONTRACTS: &[u8] = b"bridge_contracts";
+pub static NATIVE_COUNTER: &[u8] = b"native_counter";
+
+// Guardian set information
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct ConfigInfo {
+    // governance contract details
+    pub gov_chain: u16,
+    pub gov_address: Vec<u8>,
+
+    pub wormhole_contract: HumanAddr,
+    pub wrapped_asset_code_id: u64,
+}
+
+pub fn config<S: Storage>(storage: &mut S) -> Singleton<S, ConfigInfo> {
+    singleton(storage, CONFIG_KEY)
+}
+
+pub fn config_read<S: Storage>(storage: &S) -> ReadonlySingleton<S, ConfigInfo> {
+    singleton_read(storage, CONFIG_KEY)
+}
+
+pub fn bridge_contracts<S: Storage>(storage: &mut S) -> Bucket<S, Vec<u8>> {
+    bucket(BRIDGE_CONTRACTS, storage)
+}
+
+pub fn bridge_contracts_read<S: Storage>(storage: &S) -> ReadonlyBucket<S, Vec<u8>> {
+    bucket_read(BRIDGE_CONTRACTS, storage)
+}
+
+pub fn wrapped_asset<S: Storage>(storage: &mut S) -> Bucket<S, HumanAddr> {
+    bucket(WRAPPED_ASSET_KEY, storage)
+}
+
+pub fn wrapped_asset_read<S: Storage>(storage: &S) -> ReadonlyBucket<S, HumanAddr> {
+    bucket_read(WRAPPED_ASSET_KEY, storage)
+}
+
+pub fn wrapped_asset_address<S: Storage>(storage: &mut S) -> Bucket<S, Vec<u8>> {
+    bucket(WRAPPED_ASSET_ADDRESS_KEY, storage)
+}
+
+pub fn wrapped_asset_address_read<S: Storage>(storage: &S) -> ReadonlyBucket<S, Vec<u8>> {
+    bucket_read(WRAPPED_ASSET_ADDRESS_KEY, storage)
+}
+
+pub fn send_native<S: Storage>(
+    storage: &mut S,
+    asset_address: &CanonicalAddr,
+    amount: Uint128,
+) -> StdResult<()> {
+    let mut counter_bucket = bucket(NATIVE_COUNTER, storage);
+    let new_total = amount
+        + counter_bucket
+            .load(asset_address.as_slice())
+            .unwrap_or(Uint128::zero());
+    if new_total > Uint128(u64::MAX as u128) {
+        return Err(StdError::generic_err(
+            "transfer exceeds max outstanding bridged token amount",
+        ));
+    }
+    counter_bucket.save(asset_address.as_slice(), &new_total)
+}
+
+pub fn receive_native<S: Storage>(
+    storage: &mut S,
+    asset_address: &CanonicalAddr,
+    amount: Uint128,
+) -> StdResult<()> {
+    let mut counter_bucket = bucket(NATIVE_COUNTER, storage);
+    let total: Uint128 = counter_bucket.load(asset_address.as_slice())?;
+    counter_bucket.save(asset_address.as_slice(), &(total - amount)?)
+}
+
+pub struct Action;
+
+impl Action {
+    pub const TRANSFER: u8 = 1;
+    pub const ATTEST_META: u8 = 2;
+}
+
+// 0 u8 action
+// 1 [u8] payload
+
+pub struct TokenBridgeMessage {
+    pub action: u8,
+    pub payload: Vec<u8>,
+}
+
+impl TokenBridgeMessage {
+    pub fn deserialize(data: &Vec<u8>) -> StdResult<Self> {
+        let data = data.as_slice();
+        let action = data.get_u8(0);
+        let payload = &data[1..];
+
+        Ok(TokenBridgeMessage {
+            action,
+            payload: payload.to_vec(),
+        })
+    }
+
+    pub fn serialize(&self) -> Vec<u8> {
+        [self.action.to_be_bytes().to_vec(), self.payload.clone()].concat()
+    }
+}
+
+//     0   u256     amount
+//     32  [u8; 32] token_address
+//     64  u16      token_chain
+//     66  [u8; 32] recipient
+//     98  u16      recipient_chain
+//     100 u256     fee
+
+pub struct TransferInfo {
+    pub amount: (u128, u128),
+    pub token_address: Vec<u8>,
+    pub token_chain: u16,
+    pub recipient: Vec<u8>,
+    pub recipient_chain: u16,
+    pub fee: (u128, u128),
+}
+
+impl TransferInfo {
+    pub fn deserialize(data: &Vec<u8>) -> StdResult<Self> {
+        let data = data.as_slice();
+        let amount = data.get_u256(0);
+        let token_address = data.get_bytes32(32).to_vec();
+        let token_chain = data.get_u16(64);
+        let recipient = data.get_bytes32(66).to_vec();
+        let recipient_chain = data.get_u16(98);
+        let fee = data.get_u256(100);
+
+        Ok(TransferInfo {
+            amount,
+            token_address,
+            token_chain,
+            recipient,
+            recipient_chain,
+            fee,
+        })
+    }
+    pub fn serialize(&self) -> Vec<u8> {
+        [
+            self.amount.0.to_be_bytes().to_vec(),
+            self.amount.1.to_be_bytes().to_vec(),
+            self.token_address.clone(),
+            self.token_chain.to_be_bytes().to_vec(),
+            self.recipient.to_vec(),
+            self.recipient_chain.to_be_bytes().to_vec(),
+            self.fee.0.to_be_bytes().to_vec(),
+            self.fee.1.to_be_bytes().to_vec(),
+        ]
+        .concat()
+    }
+}
+
+// 0  [32]uint8  TokenAddress
+// 32 uint16     TokenChain
+// 34 uint8      Decimals
+// 35 [32]uint8  Symbol
+// 67 [32]uint8  Name
+
+pub struct AssetMeta {
+    pub token_address: Vec<u8>,
+    pub token_chain: u16,
+    pub decimals: u8,
+    pub symbol: Vec<u8>,
+    pub name: Vec<u8>,
+}
+
+impl AssetMeta {
+    pub fn deserialize(data: &Vec<u8>) -> StdResult<Self> {
+        let data = data.as_slice();
+        let token_address = data.get_bytes32(0).to_vec();
+        let token_chain = data.get_u16(32);
+        let decimals = data.get_u8(34);
+        let symbol = data.get_bytes32(35).to_vec();
+        let name = data.get_bytes32(67).to_vec();
+
+        Ok(AssetMeta {
+            token_chain,
+            token_address,
+            decimals,
+            symbol,
+            name,
+        })
+    }
+
+    pub fn serialize(&self) -> Vec<u8> {
+        [
+            self.token_address.clone(),
+            self.token_chain.to_be_bytes().to_vec(),
+            self.decimals.to_be_bytes().to_vec(),
+            self.symbol.clone(),
+            self.name.clone(),
+        ]
+        .concat()
+    }
+}
+
+pub struct RegisterChain {
+    pub chain_id: u16,
+    pub chain_address: Vec<u8>,
+}
+
+impl RegisterChain {
+    pub fn deserialize(data: &Vec<u8>) -> StdResult<Self> {
+        let data = data.as_slice();
+        let chain_id = data.get_u16(0);
+        let chain_address = data[2..].to_vec();
+
+        Ok(RegisterChain {
+            chain_id,
+            chain_address,
+        })
+    }
+}

+ 114 - 0
terra/contracts-5/token-bridge/tests/integration.rs

@@ -0,0 +1,114 @@
+static WASM: &[u8] = include_bytes!("../../../target/wasm32-unknown-unknown/release/wormhole.wasm");
+
+use cosmwasm_std::{
+    from_slice,
+    Coin,
+    Env,
+    HumanAddr,
+    InitResponse,
+};
+use cosmwasm_storage::to_length_prefixed;
+use cosmwasm_vm::{
+    testing::{
+        init,
+        mock_env,
+        mock_instance,
+        MockApi,
+        MockQuerier,
+        MockStorage,
+    },
+    Api,
+    Instance,
+    Storage,
+};
+
+use wormhole::{
+    msg::InitMsg,
+    state::{
+        ConfigInfo,
+        GuardianAddress,
+        GuardianSetInfo,
+        CONFIG_KEY,
+    },
+};
+
+use hex;
+
+enum TestAddress {
+    INITIALIZER,
+}
+
+impl TestAddress {
+    fn value(&self) -> HumanAddr {
+        match self {
+            TestAddress::INITIALIZER => HumanAddr::from("initializer"),
+        }
+    }
+}
+
+fn mock_env_height(signer: &HumanAddr, height: u64, time: u64) -> Env {
+    let mut env = mock_env(signer, &[]);
+    env.block.height = height;
+    env.block.time = time;
+    env
+}
+
+fn get_config_info<S: Storage>(storage: &S) -> ConfigInfo {
+    let key = to_length_prefixed(CONFIG_KEY);
+    let data = storage
+        .get(&key)
+        .0
+        .expect("error getting data")
+        .expect("data should exist");
+    from_slice(&data).expect("invalid data")
+}
+
+fn do_init(
+    height: u64,
+    guardians: &Vec<GuardianAddress>,
+) -> Instance<MockStorage, MockApi, MockQuerier> {
+    let mut deps = mock_instance(WASM, &[]);
+    let init_msg = InitMsg {
+        initial_guardian_set: GuardianSetInfo {
+            addresses: guardians.clone(),
+            expiration_time: 100,
+        },
+        guardian_set_expirity: 50,
+        wrapped_asset_code_id: 999,
+    };
+    let env = mock_env_height(&TestAddress::INITIALIZER.value(), height, 0);
+    let owner = deps
+        .api
+        .canonical_address(&TestAddress::INITIALIZER.value())
+        .0
+        .unwrap();
+    let res: InitResponse = init(&mut deps, env, init_msg).unwrap();
+    assert_eq!(0, res.messages.len());
+
+    // query the store directly
+    deps.with_storage(|storage| {
+        assert_eq!(
+            get_config_info(storage),
+            ConfigInfo {
+                guardian_set_index: 0,
+                guardian_set_expirity: 50,
+                wrapped_asset_code_id: 999,
+                owner,
+                fee: Coin::new(10000, "uluna"),
+            }
+        );
+        Ok(())
+    })
+    .unwrap();
+    deps
+}
+
+#[test]
+fn init_works() {
+    let guardians = vec![GuardianAddress::from(GuardianAddress {
+        bytes: hex::decode("beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe")
+            .expect("Decoding failed")
+            .into(),
+    })];
+    let _deps = do_init(111, &guardians);
+}

+ 5 - 0
terra/contracts-5/wormhole/.cargo/config

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

+ 33 - 0
terra/contracts-5/wormhole/Cargo.toml

@@ -0,0 +1,33 @@
+[package]
+name = "wormhole"
+version = "0.1.0"
+authors = ["Yuriy Savchenko <yuriy.savchenko@gmail.com>"]
+edition = "2018"
+description = "Wormhole contract"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[features]
+backtraces = ["cosmwasm-std/backtraces"]
+# use library feature to disable all init/handle/query exports
+library = []
+
+[dependencies]
+cosmwasm-std = { version = "0.10.0" }
+cosmwasm-storage = { version = "0.10.0" }
+schemars = "0.7"
+serde = { version = "1.0.103", default-features = false, features = ["derive"] }
+cw20 = "0.2.2"
+cw20-base = { version = "0.2.2", features = ["library"] }
+cw20-wrapped = { path = "../cw20-wrapped", features = ["library"] }
+thiserror = { version = "1.0.20" }
+k256 = { version = "0.5.9", default-features = false, features = ["ecdsa"] }
+sha3 = { version = "0.9.1", default-features = false }
+generic-array = { version = "0.14.4" }
+hex = "0.4.2"
+lazy_static = "1.4.0"
+
+[dev-dependencies]
+cosmwasm-vm = { version = "0.10.0", default-features = false, features = ["default-cranelift"] }
+serde_json = "1.0"

+ 76 - 0
terra/contracts-5/wormhole/src/byte_utils.rs

@@ -0,0 +1,76 @@
+use cosmwasm_std::{
+    CanonicalAddr,
+    StdError,
+    StdResult,
+};
+
+pub trait ByteUtils {
+    fn get_u8(&self, index: usize) -> u8;
+    fn get_u16(&self, index: usize) -> u16;
+    fn get_u32(&self, index: usize) -> u32;
+    fn get_u64(&self, index: usize) -> u64;
+
+    fn get_u128_be(&self, index: usize) -> u128;
+    /// High 128 then low 128
+    fn get_u256(&self, index: usize) -> (u128, u128);
+    fn get_address(&self, index: usize) -> CanonicalAddr;
+    fn get_bytes32(&self, index: usize) -> &[u8];
+}
+
+impl ByteUtils for &[u8] {
+    fn get_u8(&self, index: usize) -> u8 {
+        self[index]
+    }
+    fn get_u16(&self, index: usize) -> u16 {
+        let mut bytes: [u8; 16 / 8] = [0; 16 / 8];
+        bytes.copy_from_slice(&self[index..index + 2]);
+        u16::from_be_bytes(bytes)
+    }
+    fn get_u32(&self, index: usize) -> u32 {
+        let mut bytes: [u8; 32 / 8] = [0; 32 / 8];
+        bytes.copy_from_slice(&self[index..index + 4]);
+        u32::from_be_bytes(bytes)
+    }
+    fn get_u64(&self, index: usize) -> u64 {
+        let mut bytes: [u8; 64 / 8] = [0; 64 / 8];
+        bytes.copy_from_slice(&self[index..index + 8]);
+        u64::from_be_bytes(bytes)
+    }
+    fn get_u128_be(&self, index: usize) -> u128 {
+        let mut bytes: [u8; 128 / 8] = [0; 128 / 8];
+        bytes.copy_from_slice(&self[index..index + 128 / 8]);
+        u128::from_be_bytes(bytes)
+    }
+    fn get_u256(&self, index: usize) -> (u128, u128) {
+        (self.get_u128_be(index), self.get_u128_be(index + 128 / 8))
+    }
+    fn get_address(&self, index: usize) -> CanonicalAddr {
+        // 32 bytes are reserved for addresses, but only the last 20 bytes are taken by the actual address
+        CanonicalAddr::from(&self[index + 32 - 20..index + 32])
+    }
+    fn get_bytes32(&self, index: usize) -> &[u8] {
+        &self[index..index + 32]
+    }
+}
+
+pub fn extend_address_to_32(addr: &CanonicalAddr) -> Vec<u8> {
+    let mut result: Vec<u8> = vec![0; 12];
+    result.extend(addr.as_slice());
+    result
+}
+
+pub fn extend_string_to_32(s: &String) -> StdResult<Vec<u8>> {
+    let bytes = s.as_bytes();
+    if bytes.len() > 32 {
+        return Err(StdError::generic_err("string more than 32 "));
+    }
+
+    let result = vec![0; 32 - bytes.len()];
+    Ok([bytes.to_vec(), result].concat())
+}
+
+pub fn get_string_from_32(v: &Vec<u8>) -> StdResult<String> {
+    let s = String::from_utf8(v.clone())
+        .or_else(|_| Err(StdError::generic_err("could not parse string")))?;
+    Ok(s.chars().filter(|c| c != &'\0').collect())
+}

+ 442 - 0
terra/contracts-5/wormhole/src/contract.rs

@@ -0,0 +1,442 @@
+use cosmwasm_std::{
+    has_coins,
+    log,
+    to_binary,
+    Api,
+    BankMsg,
+    Binary,
+    Coin,
+    CosmosMsg,
+    Env,
+    Extern,
+    HandleResponse,
+    HumanAddr,
+    InitResponse,
+    Querier,
+    StdError,
+    StdResult,
+    Storage,
+};
+
+use crate::{
+    byte_utils::{
+        extend_address_to_32,
+        ByteUtils,
+    },
+    error::ContractError,
+    msg::{
+        GetAddressHexResponse,
+        GetStateResponse,
+        GuardianSetInfoResponse,
+        HandleMsg,
+        InitMsg,
+        QueryMsg,
+    },
+    state::{
+        config,
+        config_read,
+        guardian_set_get,
+        guardian_set_set,
+        sequence_read,
+        sequence_set,
+        vaa_archive_add,
+        vaa_archive_check,
+        ConfigInfo,
+        GovernancePacket,
+        GuardianAddress,
+        GuardianSetInfo,
+        GuardianSetUpgrade,
+        ParsedVAA,
+        SetFee,
+        TransferFee,
+    },
+};
+
+use k256::{
+    ecdsa::{
+        recoverable::{
+            Id as RecoverableId,
+            Signature as RecoverableSignature,
+        },
+        Signature,
+        VerifyKey,
+    },
+    EncodedPoint,
+};
+use sha3::{
+    Digest,
+    Keccak256,
+};
+
+use generic_array::GenericArray;
+use std::convert::TryFrom;
+
+// Chain ID of Terra
+const CHAIN_ID: u16 = 3;
+
+// Lock assets fee amount and denomination
+const FEE_AMOUNT: u128 = 10000;
+pub const FEE_DENOMINATION: &str = "uluna";
+
+pub fn init<S: Storage, A: Api, Q: Querier>(
+    deps: &mut Extern<S, A, Q>,
+    _env: Env,
+    msg: InitMsg,
+) -> StdResult<InitResponse> {
+    // Save general wormhole info
+    let state = ConfigInfo {
+        gov_chain: msg.gov_chain,
+        gov_address: msg.gov_address.as_slice().to_vec(),
+        guardian_set_index: 0,
+        guardian_set_expirity: msg.guardian_set_expirity,
+        fee: Coin::new(FEE_AMOUNT, FEE_DENOMINATION), // 0.01 Luna (or 10000 uluna) fee by default
+    };
+    config(&mut deps.storage).save(&state)?;
+
+    // Add initial guardian set to storage
+    guardian_set_set(
+        &mut deps.storage,
+        state.guardian_set_index,
+        &msg.initial_guardian_set,
+    )?;
+
+    Ok(InitResponse::default())
+}
+
+pub fn handle<S: Storage, A: Api, Q: Querier>(
+    deps: &mut Extern<S, A, Q>,
+    env: Env,
+    msg: HandleMsg,
+) -> StdResult<HandleResponse> {
+    match msg {
+        HandleMsg::PostMessage { message, nonce } => {
+            handle_post_message(deps, env, &message.as_slice(), nonce)
+        }
+        HandleMsg::SubmitVAA { vaa } => handle_submit_vaa(deps, env, vaa.as_slice()),
+    }
+}
+
+/// Process VAA message signed by quardians
+fn handle_submit_vaa<S: Storage, A: Api, Q: Querier>(
+    deps: &mut Extern<S, A, Q>,
+    env: Env,
+    data: &[u8],
+) -> StdResult<HandleResponse> {
+    let state = config_read(&deps.storage).load()?;
+
+    let vaa = parse_and_verify_vaa(&deps.storage, data, env.block.time)?;
+    vaa_archive_add(&mut deps.storage, vaa.hash.as_slice())?;
+
+    if state.gov_chain == vaa.emitter_chain && state.gov_address == vaa.emitter_address {
+        if state.guardian_set_index != vaa.guardian_set_index {
+            return Err(StdError::generic_err(
+                "governance VAAs must be signed by the current guardian set",
+            ));
+        }
+        return handle_governance_payload(deps, env, &vaa.payload);
+    }
+
+    ContractError::InvalidVAAAction.std_err()
+}
+
+fn handle_governance_payload<S: Storage, A: Api, Q: Querier>(
+    deps: &mut Extern<S, A, Q>,
+    env: Env,
+    data: &Vec<u8>,
+) -> StdResult<HandleResponse> {
+    let gov_packet = GovernancePacket::deserialize(&data)?;
+
+    let module = String::from_utf8(gov_packet.module).unwrap();
+    let module: String = module.chars().filter(|c| c != &'\0').collect();
+
+    if module != "Core" {
+        return Err(StdError::generic_err("this is not a valid module"));
+    }
+
+    if gov_packet.chain != 0 && gov_packet.chain != CHAIN_ID {
+        return Err(StdError::generic_err(
+            "the governance VAA is for another chain",
+        ));
+    }
+
+    match gov_packet.action {
+        // 1 is reserved for upgrade / migration
+        2u8 => vaa_update_guardian_set(deps, env, &gov_packet.payload),
+        3u8 => handle_set_fee(deps, env, &gov_packet.payload),
+        4u8 => handle_transfer_fee(deps, env, &gov_packet.payload),
+        _ => ContractError::InvalidVAAAction.std_err(),
+    }
+}
+
+/// Parses raw VAA data into a struct and verifies whether it contains sufficient signatures of an
+/// active guardian set i.e. is valid according to Wormhole consensus rules
+fn parse_and_verify_vaa<S: Storage>(
+    storage: &S,
+    data: &[u8],
+    block_time: u64,
+) -> StdResult<ParsedVAA> {
+    let vaa = ParsedVAA::deserialize(data)?;
+
+    if vaa.version != 1 {
+        return ContractError::InvalidVersion.std_err();
+    }
+
+    // Check if VAA with this hash was already accepted
+    if vaa_archive_check(storage, vaa.hash.as_slice()) {
+        return ContractError::VaaAlreadyExecuted.std_err();
+    }
+
+    // Load and check guardian set
+    let guardian_set = guardian_set_get(storage, vaa.guardian_set_index);
+    let guardian_set: GuardianSetInfo =
+        guardian_set.or_else(|_| ContractError::InvalidGuardianSetIndex.std_err())?;
+
+    if guardian_set.expiration_time != 0 && guardian_set.expiration_time < block_time {
+        return ContractError::GuardianSetExpired.std_err();
+    }
+    if (vaa.len_signers as usize) < guardian_set.quorum() {
+        return ContractError::NoQuorum.std_err();
+    }
+
+    // Verify guardian signatures
+    let mut last_index: i32 = -1;
+    let mut pos = ParsedVAA::HEADER_LEN;
+
+    for _ in 0..vaa.len_signers {
+        if pos + ParsedVAA::SIGNATURE_LEN > data.len() {
+            return ContractError::InvalidVAA.std_err();
+        }
+        let index = data.get_u8(pos) as i32;
+        if index <= last_index {
+            return ContractError::WrongGuardianIndexOrder.std_err();
+        }
+        last_index = index;
+
+        let signature = Signature::try_from(
+            &data[pos + ParsedVAA::SIG_DATA_POS
+                ..pos + ParsedVAA::SIG_DATA_POS + ParsedVAA::SIG_DATA_LEN],
+        )
+        .or_else(|_| ContractError::CannotDecodeSignature.std_err())?;
+        let id = RecoverableId::new(data.get_u8(pos + ParsedVAA::SIG_RECOVERY_POS))
+            .or_else(|_| ContractError::CannotDecodeSignature.std_err())?;
+        let recoverable_signature = RecoverableSignature::new(&signature, id)
+            .or_else(|_| ContractError::CannotDecodeSignature.std_err())?;
+
+        let verify_key = recoverable_signature
+            .recover_verify_key_from_digest_bytes(GenericArray::from_slice(vaa.hash.as_slice()))
+            .or_else(|_| ContractError::CannotRecoverKey.std_err())?;
+
+        let index = index as usize;
+        if index >= guardian_set.addresses.len() {
+            return ContractError::TooManySignatures.std_err();
+        }
+        if !keys_equal(&verify_key, &guardian_set.addresses[index]) {
+            return ContractError::GuardianSignatureError.std_err();
+        }
+        pos += ParsedVAA::SIGNATURE_LEN;
+    }
+
+    Ok(vaa)
+}
+
+fn vaa_update_guardian_set<S: Storage, A: Api, Q: Querier>(
+    deps: &mut Extern<S, A, Q>,
+    env: Env,
+    data: &Vec<u8>,
+) -> StdResult<HandleResponse> {
+    /* Payload format
+    0   uint32 new_index
+    4   uint8 len(keys)
+    5   [][20]uint8 guardian addresses
+    */
+
+    let mut state = config_read(&deps.storage).load()?;
+
+    let GuardianSetUpgrade {
+        new_guardian_set_index,
+        new_guardian_set,
+    } = GuardianSetUpgrade::deserialize(&data)?;
+
+    if new_guardian_set_index != state.guardian_set_index + 1 {
+        return ContractError::GuardianSetIndexIncreaseError.std_err();
+    }
+
+    let old_guardian_set_index = state.guardian_set_index;
+
+    state.guardian_set_index = new_guardian_set_index;
+
+    guardian_set_set(
+        &mut deps.storage,
+        state.guardian_set_index,
+        &new_guardian_set,
+    )?;
+
+    config(&mut deps.storage).save(&state)?;
+
+    let mut old_guardian_set = guardian_set_get(&deps.storage, old_guardian_set_index)?;
+    old_guardian_set.expiration_time = env.block.time + state.guardian_set_expirity;
+    guardian_set_set(&mut deps.storage, old_guardian_set_index, &old_guardian_set)?;
+
+    Ok(HandleResponse {
+        messages: vec![],
+        log: vec![
+            log("action", "guardian_set_change"),
+            log("old", old_guardian_set_index),
+            log("new", state.guardian_set_index),
+        ],
+        data: None,
+    })
+}
+
+pub fn handle_set_fee<S: Storage, A: Api, Q: Querier>(
+    deps: &mut Extern<S, A, Q>,
+    env: Env,
+    data: &Vec<u8>,
+) -> StdResult<HandleResponse> {
+    let set_fee_msg = SetFee::deserialize(&data)?;
+
+    // Save new fees
+    let mut state = config_read(&mut deps.storage).load()?;
+    state.fee = set_fee_msg.fee;
+    config(&mut deps.storage).save(&state)?;
+
+    Ok(HandleResponse {
+        messages: vec![],
+        log: vec![
+            log("action", "fee_change"),
+            log("new_fee.amount", state.fee.amount),
+            log("new_fee.denom", state.fee.denom),
+        ],
+        data: None,
+    })
+}
+
+pub fn handle_transfer_fee<S: Storage, A: Api, Q: Querier>(
+    deps: &mut Extern<S, A, Q>,
+    env: Env,
+    data: &Vec<u8>,
+) -> StdResult<HandleResponse> {
+    let transfer_msg = TransferFee::deserialize(&data)?;
+
+    Ok(HandleResponse {
+        messages: vec![CosmosMsg::Bank(BankMsg::Send {
+            from_address: env.contract.address,
+            to_address: deps.api.human_address(&transfer_msg.recipient)?,
+            amount: vec![transfer_msg.amount],
+        })],
+        log: vec![],
+        data: None,
+    })
+}
+
+fn handle_post_message<S: Storage, A: Api, Q: Querier>(
+    deps: &mut Extern<S, A, Q>,
+    env: Env,
+    message: &[u8],
+    nonce: u32,
+) -> StdResult<HandleResponse> {
+    let state = config_read(&deps.storage).load()?;
+    let fee = state.fee;
+
+    // Check fee
+    if !has_coins(env.message.sent_funds.as_ref(), &fee) {
+        return ContractError::FeeTooLow.std_err();
+    }
+
+    let emitter = extend_address_to_32(&deps.api.canonical_address(&env.message.sender)?);
+
+    let sequence = sequence_read(&deps.storage, emitter.as_slice());
+    sequence_set(&mut deps.storage, emitter.as_slice(), sequence + 1)?;
+
+    Ok(HandleResponse {
+        messages: vec![],
+        log: vec![
+            log("message.message", hex::encode(message)),
+            log("message.sender", hex::encode(emitter)),
+            log("message.chain_id", CHAIN_ID),
+            log("message.nonce", nonce),
+            log("message.sequence", sequence),
+            log("message.block_time", env.block.time),
+        ],
+        data: None,
+    })
+}
+
+pub fn query<S: Storage, A: Api, Q: Querier>(
+    deps: &Extern<S, A, Q>,
+    msg: QueryMsg,
+) -> StdResult<Binary> {
+    match msg {
+        QueryMsg::GuardianSetInfo {} => to_binary(&query_guardian_set_info(deps)?),
+        QueryMsg::VerifyVAA { vaa, block_time } => to_binary(&query_parse_and_verify_vaa(
+            deps,
+            &vaa.as_slice(),
+            block_time,
+        )?),
+        QueryMsg::GetState {} => to_binary(&query_state(deps)?),
+        QueryMsg::QueryAddressHex { address } => to_binary(&query_address_hex(deps, &address)?),
+    }
+}
+
+pub fn query_guardian_set_info<S: Storage, A: Api, Q: Querier>(
+    deps: &Extern<S, A, Q>,
+) -> StdResult<GuardianSetInfoResponse> {
+    let state = config_read(&deps.storage).load()?;
+    let guardian_set = guardian_set_get(&deps.storage, state.guardian_set_index)?;
+    let res = GuardianSetInfoResponse {
+        guardian_set_index: state.guardian_set_index,
+        addresses: guardian_set.addresses,
+    };
+    Ok(res)
+}
+
+pub fn query_parse_and_verify_vaa<S: Storage, A: Api, Q: Querier>(
+    deps: &Extern<S, A, Q>,
+    data: &[u8],
+    block_time: u64,
+) -> StdResult<ParsedVAA> {
+    parse_and_verify_vaa(&deps.storage, data, block_time)
+}
+
+// returns the hex of the 32 byte address we use for some address on this chain
+pub fn query_address_hex<S: Storage, A: Api, Q: Querier>(
+    deps: &Extern<S, A, Q>,
+    address: &HumanAddr,
+) -> StdResult<GetAddressHexResponse> {
+    Ok(GetAddressHexResponse {
+        hex: hex::encode(extend_address_to_32(&deps.api.canonical_address(&address)?)),
+    })
+}
+
+pub fn query_state<S: Storage, A: Api, Q: Querier>(
+    deps: &Extern<S, A, Q>,
+) -> StdResult<GetStateResponse> {
+    let state = config_read(&deps.storage).load()?;
+    let res = GetStateResponse { fee: state.fee };
+    Ok(res)
+}
+
+fn keys_equal(a: &VerifyKey, b: &GuardianAddress) -> bool {
+    let mut hasher = Keccak256::new();
+
+    let point: EncodedPoint = EncodedPoint::from(a);
+    let point = point.decompress();
+    if bool::from(point.is_none()) {
+        return false;
+    }
+    let point = point.unwrap();
+
+    hasher.update(&point.as_bytes()[1..]);
+    let a = &hasher.finalize()[12..];
+
+    let b = &b.bytes;
+    if a.len() != b.len() {
+        return false;
+    }
+    for (ai, bi) in a.iter().zip(b.as_slice().iter()) {
+        if ai != bi {
+            return false;
+        }
+    }
+    true
+}

+ 114 - 0
terra/contracts-5/wormhole/src/error.rs

@@ -0,0 +1,114 @@
+use cosmwasm_std::StdError;
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum ContractError {
+    /// Invalid VAA version
+    #[error("InvalidVersion")]
+    InvalidVersion,
+
+    /// Guardian set with this index does not exist
+    #[error("InvalidGuardianSetIndex")]
+    InvalidGuardianSetIndex,
+
+    /// Guardian set expiration date is zero or in the past
+    #[error("GuardianSetExpired")]
+    GuardianSetExpired,
+
+    /// Not enough signers on the VAA
+    #[error("NoQuorum")]
+    NoQuorum,
+
+    /// Wrong guardian index order, order must be ascending
+    #[error("WrongGuardianIndexOrder")]
+    WrongGuardianIndexOrder,
+
+    /// Some problem with signature decoding from bytes
+    #[error("CannotDecodeSignature")]
+    CannotDecodeSignature,
+
+    /// Some problem with public key recovery from the signature
+    #[error("CannotRecoverKey")]
+    CannotRecoverKey,
+
+    /// Recovered pubkey from signature does not match guardian address
+    #[error("GuardianSignatureError")]
+    GuardianSignatureError,
+
+    /// VAA action code not recognized
+    #[error("InvalidVAAAction")]
+    InvalidVAAAction,
+
+    /// VAA guardian set is not current
+    #[error("NotCurrentGuardianSet")]
+    NotCurrentGuardianSet,
+
+    /// Only 128-bit amounts are supported
+    #[error("AmountTooHigh")]
+    AmountTooHigh,
+
+    /// Amount should be higher than zero
+    #[error("AmountTooLow")]
+    AmountTooLow,
+
+    /// Source and target chain ids must be different
+    #[error("SameSourceAndTarget")]
+    SameSourceAndTarget,
+
+    /// Target chain id must be the same as the current CHAIN_ID
+    #[error("WrongTargetChain")]
+    WrongTargetChain,
+
+    /// Wrapped asset init hook sent twice for the same asset id
+    #[error("AssetAlreadyRegistered")]
+    AssetAlreadyRegistered,
+
+    /// Guardian set must increase in steps of 1
+    #[error("GuardianSetIndexIncreaseError")]
+    GuardianSetIndexIncreaseError,
+
+    /// VAA was already executed
+    #[error("VaaAlreadyExecuted")]
+    VaaAlreadyExecuted,
+
+    /// Message sender not permitted to execute this operation
+    #[error("PermissionDenied")]
+    PermissionDenied,
+
+    /// Could not decode target address from canonical to human-readable form
+    #[error("WrongTargetAddressFormat")]
+    WrongTargetAddressFormat,
+
+    /// More signatures than active guardians found
+    #[error("TooManySignatures")]
+    TooManySignatures,
+
+    /// Wrapped asset not found in the registry
+    #[error("AssetNotFound")]
+    AssetNotFound,
+
+    /// Generic error when there is a problem with VAA structure
+    #[error("InvalidVAA")]
+    InvalidVAA,
+
+    /// Thrown when fee is enabled for the action, but was not sent with the transaction
+    #[error("FeeTooLow")]
+    FeeTooLow,
+
+    /// Registering asset outside of the wormhole
+    #[error("RegistrationForbidden")]
+    RegistrationForbidden,
+}
+
+impl ContractError {
+    pub fn std(&self) -> StdError {
+        StdError::GenericErr {
+            msg: format!("{}", self),
+            backtrace: None,
+        }
+    }
+
+    pub fn std_err<T>(&self) -> Result<T, StdError> {
+        Err(self.std())
+    }
+}

+ 10 - 0
terra/contracts-5/wormhole/src/lib.rs

@@ -0,0 +1,10 @@
+pub mod byte_utils;
+pub mod contract;
+pub mod error;
+pub mod msg;
+pub mod state;
+
+pub use crate::error::ContractError;
+
+#[cfg(all(target_arch = "wasm32", not(feature = "library")))]
+cosmwasm_std::create_entry_points!(contract);

+ 65 - 0
terra/contracts-5/wormhole/src/msg.rs

@@ -0,0 +1,65 @@
+use cosmwasm_std::{
+    Binary,
+    Coin,
+    HumanAddr,
+};
+use schemars::JsonSchema;
+use serde::{
+    Deserialize,
+    Serialize,
+};
+
+use crate::state::{
+    GuardianAddress,
+    GuardianSetInfo,
+};
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct InitMsg {
+    pub gov_chain: u16,
+    pub gov_address: Binary,
+
+    pub initial_guardian_set: GuardianSetInfo,
+    pub guardian_set_expirity: u64,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum HandleMsg {
+    SubmitVAA { vaa: Binary },
+    PostMessage { message: Binary, nonce: u32 },
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum QueryMsg {
+    GuardianSetInfo {},
+    VerifyVAA { vaa: Binary, block_time: u64 },
+    GetState {},
+    QueryAddressHex { address: HumanAddr },
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct GuardianSetInfoResponse {
+    pub guardian_set_index: u32,         // Current guardian set index
+    pub addresses: Vec<GuardianAddress>, // List of querdian addresses
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct WrappedRegistryResponse {
+    pub address: HumanAddr,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct GetStateResponse {
+    pub fee: Coin,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct GetAddressHexResponse {
+    pub hex: String,
+}

+ 444 - 0
terra/contracts-5/wormhole/src/state.rs

@@ -0,0 +1,444 @@
+use schemars::{
+    JsonSchema,
+    Set,
+};
+use serde::{
+    Deserialize,
+    Serialize,
+};
+
+use cosmwasm_std::{
+    Binary,
+    CanonicalAddr,
+    Coin,
+    HumanAddr,
+    StdResult,
+    Storage,
+    Uint128,
+};
+use cosmwasm_storage::{
+    bucket,
+    bucket_read,
+    singleton,
+    singleton_read,
+    Bucket,
+    ReadonlyBucket,
+    ReadonlySingleton,
+    Singleton,
+};
+
+use crate::{
+    byte_utils::ByteUtils,
+    error::ContractError,
+};
+
+use sha3::{
+    Digest,
+    Keccak256,
+};
+
+pub static CONFIG_KEY: &[u8] = b"config";
+pub static GUARDIAN_SET_KEY: &[u8] = b"guardian_set";
+pub static SEQUENCE_KEY: &[u8] = b"sequence";
+pub static WRAPPED_ASSET_KEY: &[u8] = b"wrapped_asset";
+pub static WRAPPED_ASSET_ADDRESS_KEY: &[u8] = b"wrapped_asset_address";
+
+// Guardian set information
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct ConfigInfo {
+    // Current active guardian set
+    pub guardian_set_index: u32,
+
+    // Period for which a guardian set stays active after it has been replaced
+    pub guardian_set_expirity: u64,
+
+    // governance contract details
+    pub gov_chain: u16,
+    pub gov_address: Vec<u8>,
+
+    // Message sending fee
+    pub fee: Coin,
+}
+
+// Validator Action Approval(VAA) data
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct ParsedVAA {
+    pub version: u8,
+    pub guardian_set_index: u32,
+    pub timestamp: u32,
+    pub nonce: u32,
+    pub len_signers: u8,
+
+    pub emitter_chain: u16,
+    pub emitter_address: Vec<u8>,
+    pub sequence: u64,
+    pub consistency_level: u8,
+    pub payload: Vec<u8>,
+
+    pub hash: Vec<u8>,
+}
+
+impl ParsedVAA {
+    /* VAA format:
+
+    header (length 6):
+    0   uint8   version (0x01)
+    1   uint32  guardian set index
+    5   uint8   len signatures
+
+    per signature (length 66):
+    0   uint8       index of the signer (in guardian keys)
+    1   [65]uint8   signature
+
+    body:
+    0   uint32      timestamp (unix in seconds)
+    4   uint32      nonce
+    8   uint16      emitter_chain
+    10  [32]uint8   emitter_address
+    42  uint64      sequence
+    50  uint8       consistency_level
+    51  []uint8     payload
+    */
+
+    pub const HEADER_LEN: usize = 6;
+    pub const SIGNATURE_LEN: usize = 66;
+
+    pub const GUARDIAN_SET_INDEX_POS: usize = 1;
+    pub const LEN_SIGNER_POS: usize = 5;
+
+    pub const VAA_NONCE_POS: usize = 4;
+    pub const VAA_EMITTER_CHAIN_POS: usize = 8;
+    pub const VAA_EMITTER_ADDRESS_POS: usize = 10;
+    pub const VAA_SEQUENCE_POS: usize = 42;
+    pub const VAA_CONSISTENCY_LEVEL_POS: usize = 50;
+    pub const VAA_PAYLOAD_POS: usize = 51;
+
+    // Signature data offsets in the signature block
+    pub const SIG_DATA_POS: usize = 1;
+    // Signature length minus recovery id at the end
+    pub const SIG_DATA_LEN: usize = 64;
+    // Recovery byte is last after the main signature
+    pub const SIG_RECOVERY_POS: usize = Self::SIG_DATA_POS + Self::SIG_DATA_LEN;
+
+    pub fn deserialize(data: &[u8]) -> StdResult<Self> {
+        let version = data.get_u8(0);
+
+        // Load 4 bytes starting from index 1
+        let guardian_set_index: u32 = data.get_u32(Self::GUARDIAN_SET_INDEX_POS);
+        let len_signers = data.get_u8(Self::LEN_SIGNER_POS) as usize;
+        let body_offset: usize = Self::HEADER_LEN + Self::SIGNATURE_LEN * len_signers as usize;
+
+        // Hash the body
+        if body_offset >= data.len() {
+            return ContractError::InvalidVAA.std_err();
+        }
+        let body = &data[body_offset..];
+        let mut hasher = Keccak256::new();
+        hasher.update(body);
+        let hash = hasher.finalize().to_vec();
+
+        // Rehash the hash
+        let mut hasher = Keccak256::new();
+        hasher.update(hash);
+        let hash = hasher.finalize().to_vec();
+
+        // Signatures valid, apply VAA
+        if body_offset + Self::VAA_PAYLOAD_POS > data.len() {
+            return ContractError::InvalidVAA.std_err();
+        }
+
+        let timestamp = data.get_u32(body_offset);
+        let nonce = data.get_u32(body_offset + Self::VAA_NONCE_POS);
+        let emitter_chain = data.get_u16(body_offset + Self::VAA_EMITTER_CHAIN_POS);
+        let emitter_address = data
+            .get_bytes32(body_offset + Self::VAA_EMITTER_ADDRESS_POS)
+            .to_vec();
+        let sequence = data.get_u64(body_offset + Self::VAA_SEQUENCE_POS);
+        let consistency_level = data.get_u8(body_offset + Self::VAA_CONSISTENCY_LEVEL_POS);
+        let payload = data[body_offset + Self::VAA_PAYLOAD_POS..].to_vec();
+
+        Ok(ParsedVAA {
+            version,
+            guardian_set_index,
+            timestamp,
+            nonce,
+            len_signers: len_signers as u8,
+            emitter_chain,
+            emitter_address,
+            sequence,
+            consistency_level,
+            payload,
+            hash,
+        })
+    }
+}
+
+// Guardian address
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct GuardianAddress {
+    pub bytes: Binary, // 20-byte addresses
+}
+
+use crate::contract::FEE_DENOMINATION;
+#[cfg(test)]
+use hex;
+
+#[cfg(test)]
+impl GuardianAddress {
+    pub fn from(string: &str) -> GuardianAddress {
+        GuardianAddress {
+            bytes: hex::decode(string).expect("Decoding failed").into(),
+        }
+    }
+}
+
+// Guardian set information
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct GuardianSetInfo {
+    pub addresses: Vec<GuardianAddress>,
+    // List of guardian addresses
+    pub expiration_time: u64, // Guardian set expiration time
+}
+
+impl GuardianSetInfo {
+    pub fn quorum(&self) -> usize {
+        // allow quorum of 0 for testing purposes...
+        if self.addresses.len() == 0 {
+            return 0;
+        }
+        ((self.addresses.len() * 10 / 3) * 2) / 10 + 1
+    }
+}
+
+// Wormhole contract generic information
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct WormholeInfo {
+    // Period for which a guardian set stays active after it has been replaced
+    pub guardian_set_expirity: u64,
+}
+
+pub fn config<S: Storage>(storage: &mut S) -> Singleton<S, ConfigInfo> {
+    singleton(storage, CONFIG_KEY)
+}
+
+pub fn config_read<S: Storage>(storage: &S) -> ReadonlySingleton<S, ConfigInfo> {
+    singleton_read(storage, CONFIG_KEY)
+}
+
+pub fn guardian_set_set<S: Storage>(
+    storage: &mut S,
+    index: u32,
+    data: &GuardianSetInfo,
+) -> StdResult<()> {
+    bucket(GUARDIAN_SET_KEY, storage).save(&index.to_be_bytes(), data)
+}
+
+pub fn guardian_set_get<S: Storage>(storage: &S, index: u32) -> StdResult<GuardianSetInfo> {
+    bucket_read(GUARDIAN_SET_KEY, storage).load(&index.to_be_bytes())
+}
+
+pub fn sequence_set<S: Storage>(storage: &mut S, emitter: &[u8], sequence: u64) -> StdResult<()> {
+    bucket(SEQUENCE_KEY, storage).save(emitter, &sequence)
+}
+
+pub fn sequence_read<S: Storage>(storage: &S, emitter: &[u8]) -> u64 {
+    bucket_read(SEQUENCE_KEY, storage)
+        .load(&emitter)
+        .or::<u64>(Ok(0))
+        .unwrap()
+}
+
+pub fn vaa_archive_add<S: Storage>(storage: &mut S, hash: &[u8]) -> StdResult<()> {
+    bucket(GUARDIAN_SET_KEY, storage).save(hash, &true)
+}
+
+pub fn vaa_archive_check<S: Storage>(storage: &S, hash: &[u8]) -> bool {
+    bucket_read(GUARDIAN_SET_KEY, storage)
+        .load(&hash)
+        .or::<bool>(Ok(false))
+        .unwrap()
+}
+
+pub fn wrapped_asset<S: Storage>(storage: &mut S) -> Bucket<S, HumanAddr> {
+    bucket(WRAPPED_ASSET_KEY, storage)
+}
+
+pub fn wrapped_asset_read<S: Storage>(storage: &S) -> ReadonlyBucket<S, HumanAddr> {
+    bucket_read(WRAPPED_ASSET_KEY, storage)
+}
+
+pub fn wrapped_asset_address<S: Storage>(storage: &mut S) -> Bucket<S, Vec<u8>> {
+    bucket(WRAPPED_ASSET_ADDRESS_KEY, storage)
+}
+
+pub fn wrapped_asset_address_read<S: Storage>(storage: &S) -> ReadonlyBucket<S, Vec<u8>> {
+    bucket_read(WRAPPED_ASSET_ADDRESS_KEY, storage)
+}
+
+pub struct GovernancePacket {
+    pub module: Vec<u8>,
+    pub action: u8,
+    pub chain: u16,
+    pub payload: Vec<u8>,
+}
+
+impl GovernancePacket {
+    pub fn deserialize(data: &Vec<u8>) -> StdResult<Self> {
+        let data = data.as_slice();
+        let module = data.get_bytes32(0).to_vec();
+        let action = data.get_u8(32);
+        let chain = data.get_u16(33);
+        let payload = data[35..].to_vec();
+
+        Ok(GovernancePacket {
+            module,
+            action,
+            chain,
+            payload,
+        })
+    }
+}
+
+// action 2
+pub struct GuardianSetUpgrade {
+    pub new_guardian_set_index: u32,
+    pub new_guardian_set: GuardianSetInfo,
+}
+
+impl GuardianSetUpgrade {
+    pub fn deserialize(data: &Vec<u8>) -> StdResult<Self> {
+        const ADDRESS_LEN: usize = 20;
+
+        let data = data.as_slice();
+        let new_guardian_set_index = data.get_u32(0);
+
+        let n_guardians = data.get_u8(4);
+
+        let mut addresses = vec![];
+
+        for i in 0..n_guardians {
+            let pos = 5 + (i as usize) * ADDRESS_LEN;
+            if pos + ADDRESS_LEN > data.len() {
+                return ContractError::InvalidVAA.std_err();
+            }
+
+            addresses.push(GuardianAddress {
+                bytes: data[pos..pos + ADDRESS_LEN].to_vec().into(),
+            });
+        }
+
+        let new_guardian_set = GuardianSetInfo {
+            addresses,
+            expiration_time: 0,
+        };
+
+        return Ok(GuardianSetUpgrade {
+            new_guardian_set_index,
+            new_guardian_set,
+        });
+    }
+}
+
+// action 3
+pub struct SetFee {
+    pub fee: Coin,
+}
+
+impl SetFee {
+    pub fn deserialize(data: &Vec<u8>) -> StdResult<Self> {
+        let data = data.as_slice();
+
+        let (_, amount) = data.get_u256(0);
+        let fee = Coin {
+            denom: String::from(FEE_DENOMINATION),
+            amount: Uint128(amount),
+        };
+        Ok(SetFee { fee })
+    }
+}
+
+// action 4
+pub struct TransferFee {
+    pub amount: Coin,
+    pub recipient: CanonicalAddr,
+}
+
+impl TransferFee {
+    pub fn deserialize(data: &Vec<u8>) -> StdResult<Self> {
+        let data = data.as_slice();
+        let recipient = data.get_address(0);
+
+        let (_, amount) = data.get_u256(32);
+        let amount = Coin {
+            denom: String::from(FEE_DENOMINATION),
+            amount: Uint128(amount),
+        };
+        Ok(TransferFee { amount, recipient })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn build_guardian_set(length: usize) -> GuardianSetInfo {
+        let mut addresses: Vec<GuardianAddress> = Vec::with_capacity(length);
+        for _ in 0..length {
+            addresses.push(GuardianAddress {
+                bytes: vec![].into(),
+            });
+        }
+
+        GuardianSetInfo {
+            addresses,
+            expiration_time: 0,
+        }
+    }
+
+    #[test]
+    fn quardian_set_quorum() {
+        assert_eq!(build_guardian_set(1).quorum(), 1);
+        assert_eq!(build_guardian_set(2).quorum(), 2);
+        assert_eq!(build_guardian_set(3).quorum(), 3);
+        assert_eq!(build_guardian_set(4).quorum(), 3);
+        assert_eq!(build_guardian_set(5).quorum(), 4);
+        assert_eq!(build_guardian_set(6).quorum(), 5);
+        assert_eq!(build_guardian_set(7).quorum(), 5);
+        assert_eq!(build_guardian_set(8).quorum(), 6);
+        assert_eq!(build_guardian_set(9).quorum(), 7);
+        assert_eq!(build_guardian_set(10).quorum(), 7);
+        assert_eq!(build_guardian_set(11).quorum(), 8);
+        assert_eq!(build_guardian_set(12).quorum(), 9);
+        assert_eq!(build_guardian_set(20).quorum(), 14);
+        assert_eq!(build_guardian_set(25).quorum(), 17);
+        assert_eq!(build_guardian_set(100).quorum(), 67);
+    }
+
+    #[test]
+    fn test_deserialize() {
+        let x = hex::decode("080000000901007bfa71192f886ab6819fa4862e34b4d178962958d9b2e3d9437338c9e5fde1443b809d2886eaa69e0f0158ea517675d96243c9209c3fe1d94d5b19866654c6980000000b150000000500020001020304000000000000000000000000000000000000000000000000000000000000000000000a0261626364").unwrap();
+        let v = ParsedVAA::deserialize(x.as_slice()).unwrap();
+        assert_eq!(
+            v,
+            ParsedVAA {
+                version: 8,
+                guardian_set_index: 9,
+                timestamp: 2837,
+                nonce: 5,
+                len_signers: 1,
+                emitter_chain: 2,
+                emitter_address: vec![
+                    0, 1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                    0, 0, 0, 0, 0, 0
+                ],
+                sequence: 10,
+                consistency_level: 2,
+                payload: vec![97, 98, 99, 100],
+                hash: vec![
+                    195, 10, 19, 96, 8, 61, 218, 69, 160, 238, 165, 142, 105, 119, 139, 121, 212,
+                    73, 238, 179, 13, 80, 245, 224, 75, 110, 163, 8, 185, 132, 55, 34
+                ]
+            }
+        );
+    }
+}

+ 11 - 0
terra/rustfmt.toml

@@ -0,0 +1,11 @@
+# Merge similar crates together to avoid multiple use statements.
+imports_granularity = "Crate"
+
+# Consistency in formatting makes tool based searching/editing better.
+empty_item_single_line = false
+
+# Easier editing when arbitrary mixed use statements do not collapse.
+imports_layout = "Vertical"
+
+# Default rustfmt formatting of match arms with branches is awful.
+match_arm_leading_pipes = "Preserve"