From ce6d92bb2bca1642b0164b7a268ffa0eb789f831 Mon Sep 17 00:00:00 2001 From: Reisen Date: Mon, 6 Sep 2021 08:27:27 +0000 Subject: [PATCH] terra/contracts: add columbus-5 clone Change-Id: I83c2387bd85f524962229e7b0ec72976f25d3396 --- terra/contracts-5/README.md | 3 + terra/contracts-5/cw20-wrapped/.cargo/config | 5 + terra/contracts-5/cw20-wrapped/Cargo.toml | 27 + .../contracts-5/cw20-wrapped/src/contract.rs | 359 ++++++++ terra/contracts-5/cw20-wrapped/src/error.rs | 27 + terra/contracts-5/cw20-wrapped/src/lib.rs | 9 + terra/contracts-5/cw20-wrapped/src/msg.rs | 120 +++ terra/contracts-5/cw20-wrapped/src/state.rs | 38 + .../cw20-wrapped/tests/integration.rs | 253 ++++++ terra/contracts-5/token-bridge/.cargo/config | 5 + terra/contracts-5/token-bridge/Cargo.toml | 37 + .../contracts-5/token-bridge/src/contract.rs | 777 ++++++++++++++++++ terra/contracts-5/token-bridge/src/lib.rs | 10 + terra/contracts-5/token-bridge/src/msg.rs | 64 ++ terra/contracts-5/token-bridge/src/state.rs | 247 ++++++ .../token-bridge/tests/integration.rs | 114 +++ terra/contracts-5/wormhole/.cargo/config | 5 + terra/contracts-5/wormhole/Cargo.toml | 33 + terra/contracts-5/wormhole/src/byte_utils.rs | 76 ++ terra/contracts-5/wormhole/src/contract.rs | 442 ++++++++++ terra/contracts-5/wormhole/src/error.rs | 114 +++ terra/contracts-5/wormhole/src/lib.rs | 10 + terra/contracts-5/wormhole/src/msg.rs | 65 ++ terra/contracts-5/wormhole/src/state.rs | 444 ++++++++++ terra/rustfmt.toml | 11 + 25 files changed, 3295 insertions(+) create mode 100644 terra/contracts-5/README.md create mode 100644 terra/contracts-5/cw20-wrapped/.cargo/config create mode 100644 terra/contracts-5/cw20-wrapped/Cargo.toml create mode 100644 terra/contracts-5/cw20-wrapped/src/contract.rs create mode 100644 terra/contracts-5/cw20-wrapped/src/error.rs create mode 100644 terra/contracts-5/cw20-wrapped/src/lib.rs create mode 100644 terra/contracts-5/cw20-wrapped/src/msg.rs create mode 100644 terra/contracts-5/cw20-wrapped/src/state.rs create mode 100644 terra/contracts-5/cw20-wrapped/tests/integration.rs create mode 100644 terra/contracts-5/token-bridge/.cargo/config create mode 100644 terra/contracts-5/token-bridge/Cargo.toml create mode 100644 terra/contracts-5/token-bridge/src/contract.rs create mode 100644 terra/contracts-5/token-bridge/src/lib.rs create mode 100644 terra/contracts-5/token-bridge/src/msg.rs create mode 100644 terra/contracts-5/token-bridge/src/state.rs create mode 100644 terra/contracts-5/token-bridge/tests/integration.rs create mode 100644 terra/contracts-5/wormhole/.cargo/config create mode 100644 terra/contracts-5/wormhole/Cargo.toml create mode 100644 terra/contracts-5/wormhole/src/byte_utils.rs create mode 100644 terra/contracts-5/wormhole/src/contract.rs create mode 100644 terra/contracts-5/wormhole/src/error.rs create mode 100644 terra/contracts-5/wormhole/src/lib.rs create mode 100644 terra/contracts-5/wormhole/src/msg.rs create mode 100644 terra/contracts-5/wormhole/src/state.rs create mode 100644 terra/rustfmt.toml diff --git a/terra/contracts-5/README.md b/terra/contracts-5/README.md new file mode 100644 index 000000000..27ec9c8e1 --- /dev/null +++ b/terra/contracts-5/README.md @@ -0,0 +1,3 @@ +# Terra Wormhole Contracts + +The Wormhole Terra integration is developed and maintained by Everstake / @ysavchenko. diff --git a/terra/contracts-5/cw20-wrapped/.cargo/config b/terra/contracts-5/cw20-wrapped/.cargo/config new file mode 100644 index 000000000..2d5cce4ea --- /dev/null +++ b/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" \ No newline at end of file diff --git a/terra/contracts-5/cw20-wrapped/Cargo.toml b/terra/contracts-5/cw20-wrapped/Cargo.toml new file mode 100644 index 000000000..c342d6f8c --- /dev/null +++ b/terra/contracts-5/cw20-wrapped/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "cw20-wrapped" +version = "0.1.0" +authors = ["Yuriy Savchenko "] +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 } diff --git a/terra/contracts-5/cw20-wrapped/src/contract.rs b/terra/contracts-5/cw20-wrapped/src/contract.rs new file mode 100644 index 000000000..4c053b773 --- /dev/null +++ b/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( + deps: &mut Extern, + env: Env, + msg: InitMsg, +) -> StdResult { + // 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( + deps: &mut Extern, + env: Env, + msg: HandleMsg, +) -> StdResult { + 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( + deps: &mut Extern, + env: Env, + recipient: HumanAddr, + amount: Uint128, +) -> StdResult { + // 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( + deps: &Extern, + msg: QueryMsg, +) -> StdResult { + 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( + deps: &Extern, +) -> StdResult { + 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( + deps: &Extern, +) -> StdResult { + 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>( + deps: &Extern, + address: T, + ) -> Uint128 { + query_balance(&deps, address.into()).unwrap().balance + } + + fn do_init(deps: &mut Extern, 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( + deps: &mut Extern, + 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 + } +} diff --git a/terra/contracts-5/cw20-wrapped/src/error.rs b/terra/contracts-5/cw20-wrapped/src/error.rs new file mode 100644 index 000000000..95eba93cb --- /dev/null +++ b/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 {}, +} diff --git a/terra/contracts-5/cw20-wrapped/src/lib.rs b/terra/contracts-5/cw20-wrapped/src/lib.rs new file mode 100644 index 000000000..e8a832763 --- /dev/null +++ b/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); diff --git a/terra/contracts-5/cw20-wrapped/src/msg.rs b/terra/contracts-5/cw20-wrapped/src/msg.rs new file mode 100644 index 000000000..31c8cd6e0 --- /dev/null +++ b/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, + pub init_hook: Option, +} + +#[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, + }, + /// 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, + }, + /// 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, + }, + /// 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, + }, + /// 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 +} diff --git a/terra/contracts-5/cw20-wrapped/src/state.rs b/terra/contracts-5/cw20-wrapped/src/state.rs new file mode 100644 index 000000000..382f7d83c --- /dev/null +++ b/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(storage: &mut S) -> Singleton { + singleton(storage, KEY_WRAPPED_ASSET) +} + +pub fn wrapped_asset_info_read( + storage: &S, +) -> ReadonlySingleton { + singleton_read(storage, KEY_WRAPPED_ASSET) +} diff --git a/terra/contracts-5/cw20-wrapped/tests/integration.rs b/terra/contracts-5/cw20-wrapped/tests/integration.rs new file mode 100644 index 000000000..141cbea50 --- /dev/null +++ b/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(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 { + 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, + 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, + 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, + 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, 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 = 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), + ); +} diff --git a/terra/contracts-5/token-bridge/.cargo/config b/terra/contracts-5/token-bridge/.cargo/config new file mode 100644 index 000000000..2d5cce4ea --- /dev/null +++ b/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" \ No newline at end of file diff --git a/terra/contracts-5/token-bridge/Cargo.toml b/terra/contracts-5/token-bridge/Cargo.toml new file mode 100644 index 000000000..a0df81ecf --- /dev/null +++ b/terra/contracts-5/token-bridge/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "token-bridge" +version = "0.1.0" +authors = ["Yuriy Savchenko "] +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" \ No newline at end of file diff --git a/terra/contracts-5/token-bridge/src/contract.rs b/terra/contracts-5/token-bridge/src/contract.rs new file mode 100644 index 000000000..de3f7b150 --- /dev/null +++ b/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( + deps: &mut Extern, + _env: Env, + msg: InitMsg, +) -> StdResult { + // 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( + deps: &mut Extern, + coins: Vec, +) -> StdResult> { + 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( + deps: &mut Extern, + block_time: u64, + data: &Binary, +) -> StdResult { + 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( + deps: &mut Extern, + env: Env, + msg: HandleMsg, +) -> StdResult { + 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( + deps: &mut Extern, + env: Env, + asset_id: &[u8], +) -> StdResult { + 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( + deps: &mut Extern, + env: Env, + emitter_chain: u16, + emitter_address: Vec, + data: &Vec, +) -> StdResult { + 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( + deps: &mut Extern, + env: Env, + asset_address: &HumanAddr, + nonce: u32, +) -> StdResult { + 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( + deps: &mut Extern, + env: Env, + data: &Binary, +) -> StdResult { + 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( + deps: &mut Extern, + env: Env, + data: &Vec, +) -> StdResult { + 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( + deps: &mut Extern, + env: Env, + data: &Vec, +) -> StdResult { + 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( + deps: &mut Extern, + env: Env, + emitter_chain: u16, + emitter_address: Vec, + data: &Vec, +) -> StdResult { + 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( + deps: &mut Extern, + env: Env, + asset: HumanAddr, + mut amount: Uint128, + recipient_chain: u16, + recipient: Vec, + mut fee: Uint128, + nonce: u32, +) -> StdResult { + 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; + + let cfg: ConfigInfo = config_read(&deps.storage).load()?; + let asset_canonical: CanonicalAddr = deps.api.canonical_address(&asset)?; + + let mut messages: Vec = 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( + deps: &Extern, + msg: QueryMsg, +) -> StdResult { + match msg { + QueryMsg::WrappedRegistry { chain, address } => { + to_binary(&query_wrapped_registry(deps, chain, address.as_slice())?) + } + } +} + +pub fn query_wrapped_registry( + deps: &Extern, + chain: u16, + address: &[u8], +) -> StdResult { + 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 { + let mut asset_id: Vec = 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(()) + } +} diff --git a/terra/contracts-5/token-bridge/src/lib.rs b/terra/contracts-5/token-bridge/src/lib.rs new file mode 100644 index 000000000..1d2cc8b37 --- /dev/null +++ b/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); diff --git a/terra/contracts-5/token-bridge/src/msg.rs b/terra/contracts-5/token-bridge/src/msg.rs new file mode 100644 index 000000000..bbb7c2a4a --- /dev/null +++ b/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 }, +} diff --git a/terra/contracts-5/token-bridge/src/state.rs b/terra/contracts-5/token-bridge/src/state.rs new file mode 100644 index 000000000..492ecc3b8 --- /dev/null +++ b/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, + + pub wormhole_contract: HumanAddr, + pub wrapped_asset_code_id: u64, +} + +pub fn config(storage: &mut S) -> Singleton { + singleton(storage, CONFIG_KEY) +} + +pub fn config_read(storage: &S) -> ReadonlySingleton { + singleton_read(storage, CONFIG_KEY) +} + +pub fn bridge_contracts(storage: &mut S) -> Bucket> { + bucket(BRIDGE_CONTRACTS, storage) +} + +pub fn bridge_contracts_read(storage: &S) -> ReadonlyBucket> { + bucket_read(BRIDGE_CONTRACTS, storage) +} + +pub fn wrapped_asset(storage: &mut S) -> Bucket { + bucket(WRAPPED_ASSET_KEY, storage) +} + +pub fn wrapped_asset_read(storage: &S) -> ReadonlyBucket { + bucket_read(WRAPPED_ASSET_KEY, storage) +} + +pub fn wrapped_asset_address(storage: &mut S) -> Bucket> { + bucket(WRAPPED_ASSET_ADDRESS_KEY, storage) +} + +pub fn wrapped_asset_address_read(storage: &S) -> ReadonlyBucket> { + bucket_read(WRAPPED_ASSET_ADDRESS_KEY, storage) +} + +pub fn send_native( + 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( + 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, +} + +impl TokenBridgeMessage { + pub fn deserialize(data: &Vec) -> StdResult { + 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 { + [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, + pub token_chain: u16, + pub recipient: Vec, + pub recipient_chain: u16, + pub fee: (u128, u128), +} + +impl TransferInfo { + pub fn deserialize(data: &Vec) -> StdResult { + 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 { + [ + 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, + pub token_chain: u16, + pub decimals: u8, + pub symbol: Vec, + pub name: Vec, +} + +impl AssetMeta { + pub fn deserialize(data: &Vec) -> StdResult { + 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 { + [ + 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, +} + +impl RegisterChain { + pub fn deserialize(data: &Vec) -> StdResult { + 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, + }) + } +} diff --git a/terra/contracts-5/token-bridge/tests/integration.rs b/terra/contracts-5/token-bridge/tests/integration.rs new file mode 100644 index 000000000..88d0359ba --- /dev/null +++ b/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(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, +) -> Instance { + 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); +} diff --git a/terra/contracts-5/wormhole/.cargo/config b/terra/contracts-5/wormhole/.cargo/config new file mode 100644 index 000000000..2d5cce4ea --- /dev/null +++ b/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" \ No newline at end of file diff --git a/terra/contracts-5/wormhole/Cargo.toml b/terra/contracts-5/wormhole/Cargo.toml new file mode 100644 index 000000000..391fdd4ca --- /dev/null +++ b/terra/contracts-5/wormhole/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "wormhole" +version = "0.1.0" +authors = ["Yuriy Savchenko "] +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" \ No newline at end of file diff --git a/terra/contracts-5/wormhole/src/byte_utils.rs b/terra/contracts-5/wormhole/src/byte_utils.rs new file mode 100644 index 000000000..9f3a0e636 --- /dev/null +++ b/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 { + let mut result: Vec = vec![0; 12]; + result.extend(addr.as_slice()); + result +} + +pub fn extend_string_to_32(s: &String) -> StdResult> { + 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) -> StdResult { + 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()) +} diff --git a/terra/contracts-5/wormhole/src/contract.rs b/terra/contracts-5/wormhole/src/contract.rs new file mode 100644 index 000000000..c1859594a --- /dev/null +++ b/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( + deps: &mut Extern, + _env: Env, + msg: InitMsg, +) -> StdResult { + // 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( + deps: &mut Extern, + env: Env, + msg: HandleMsg, +) -> StdResult { + 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( + deps: &mut Extern, + env: Env, + data: &[u8], +) -> StdResult { + 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( + deps: &mut Extern, + env: Env, + data: &Vec, +) -> StdResult { + 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( + storage: &S, + data: &[u8], + block_time: u64, +) -> StdResult { + 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( + deps: &mut Extern, + env: Env, + data: &Vec, +) -> StdResult { + /* 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( + deps: &mut Extern, + env: Env, + data: &Vec, +) -> StdResult { + 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( + deps: &mut Extern, + env: Env, + data: &Vec, +) -> StdResult { + 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( + deps: &mut Extern, + env: Env, + message: &[u8], + nonce: u32, +) -> StdResult { + 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( + deps: &Extern, + msg: QueryMsg, +) -> StdResult { + 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( + deps: &Extern, +) -> StdResult { + 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( + deps: &Extern, + data: &[u8], + block_time: u64, +) -> StdResult { + 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( + deps: &Extern, + address: &HumanAddr, +) -> StdResult { + Ok(GetAddressHexResponse { + hex: hex::encode(extend_address_to_32(&deps.api.canonical_address(&address)?)), + }) +} + +pub fn query_state( + deps: &Extern, +) -> StdResult { + 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 +} diff --git a/terra/contracts-5/wormhole/src/error.rs b/terra/contracts-5/wormhole/src/error.rs new file mode 100644 index 000000000..2975eafc4 --- /dev/null +++ b/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(&self) -> Result { + Err(self.std()) + } +} diff --git a/terra/contracts-5/wormhole/src/lib.rs b/terra/contracts-5/wormhole/src/lib.rs new file mode 100644 index 000000000..9117b3f19 --- /dev/null +++ b/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); diff --git a/terra/contracts-5/wormhole/src/msg.rs b/terra/contracts-5/wormhole/src/msg.rs new file mode 100644 index 000000000..0bfc5c13f --- /dev/null +++ b/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, // 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, +} diff --git a/terra/contracts-5/wormhole/src/state.rs b/terra/contracts-5/wormhole/src/state.rs new file mode 100644 index 000000000..e8abe94e7 --- /dev/null +++ b/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, + + // 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, + pub sequence: u64, + pub consistency_level: u8, + pub payload: Vec, + + pub hash: Vec, +} + +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 { + 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, + // 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(storage: &mut S) -> Singleton { + singleton(storage, CONFIG_KEY) +} + +pub fn config_read(storage: &S) -> ReadonlySingleton { + singleton_read(storage, CONFIG_KEY) +} + +pub fn guardian_set_set( + storage: &mut S, + index: u32, + data: &GuardianSetInfo, +) -> StdResult<()> { + bucket(GUARDIAN_SET_KEY, storage).save(&index.to_be_bytes(), data) +} + +pub fn guardian_set_get(storage: &S, index: u32) -> StdResult { + bucket_read(GUARDIAN_SET_KEY, storage).load(&index.to_be_bytes()) +} + +pub fn sequence_set(storage: &mut S, emitter: &[u8], sequence: u64) -> StdResult<()> { + bucket(SEQUENCE_KEY, storage).save(emitter, &sequence) +} + +pub fn sequence_read(storage: &S, emitter: &[u8]) -> u64 { + bucket_read(SEQUENCE_KEY, storage) + .load(&emitter) + .or::(Ok(0)) + .unwrap() +} + +pub fn vaa_archive_add(storage: &mut S, hash: &[u8]) -> StdResult<()> { + bucket(GUARDIAN_SET_KEY, storage).save(hash, &true) +} + +pub fn vaa_archive_check(storage: &S, hash: &[u8]) -> bool { + bucket_read(GUARDIAN_SET_KEY, storage) + .load(&hash) + .or::(Ok(false)) + .unwrap() +} + +pub fn wrapped_asset(storage: &mut S) -> Bucket { + bucket(WRAPPED_ASSET_KEY, storage) +} + +pub fn wrapped_asset_read(storage: &S) -> ReadonlyBucket { + bucket_read(WRAPPED_ASSET_KEY, storage) +} + +pub fn wrapped_asset_address(storage: &mut S) -> Bucket> { + bucket(WRAPPED_ASSET_ADDRESS_KEY, storage) +} + +pub fn wrapped_asset_address_read(storage: &S) -> ReadonlyBucket> { + bucket_read(WRAPPED_ASSET_ADDRESS_KEY, storage) +} + +pub struct GovernancePacket { + pub module: Vec, + pub action: u8, + pub chain: u16, + pub payload: Vec, +} + +impl GovernancePacket { + pub fn deserialize(data: &Vec) -> StdResult { + 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) -> StdResult { + 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) -> StdResult { + 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) -> StdResult { + 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 = 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 + ] + } + ); + } +} diff --git a/terra/rustfmt.toml b/terra/rustfmt.toml new file mode 100644 index 000000000..3b9ff103f --- /dev/null +++ b/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"