terra/contracts: add columbus-5 clone
Change-Id: I83c2387bd85f524962229e7b0ec72976f25d3396
This commit is contained in:
parent
50bd0355fd
commit
ce6d92bb2b
|
@ -0,0 +1,3 @@
|
||||||
|
# Terra Wormhole Contracts
|
||||||
|
|
||||||
|
The Wormhole Terra integration is developed and maintained by Everstake / @ysavchenko.
|
|
@ -0,0 +1,5 @@
|
||||||
|
[alias]
|
||||||
|
wasm = "build --release --target wasm32-unknown-unknown"
|
||||||
|
wasm-debug = "build --target wasm32-unknown-unknown"
|
||||||
|
unit-test = "test --lib --features backtraces"
|
||||||
|
integration-test = "test --test integration"
|
|
@ -0,0 +1,27 @@
|
||||||
|
[package]
|
||||||
|
name = "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 }
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {},
|
||||||
|
}
|
|
@ -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);
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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),
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
[alias]
|
||||||
|
wasm = "build --release --target wasm32-unknown-unknown"
|
||||||
|
wasm-debug = "build --target wasm32-unknown-unknown"
|
||||||
|
unit-test = "test --lib --features backtraces"
|
||||||
|
integration-test = "test --test integration"
|
|
@ -0,0 +1,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"
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
@ -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 },
|
||||||
|
}
|
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
[alias]
|
||||||
|
wasm = "build --release --target wasm32-unknown-unknown"
|
||||||
|
wasm-debug = "build --target wasm32-unknown-unknown"
|
||||||
|
unit-test = "test --lib --features backtraces"
|
||||||
|
integration-test = "test --test integration"
|
|
@ -0,0 +1,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"
|
|
@ -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())
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
@ -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,
|
||||||
|
}
|
|
@ -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
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
Loading…
Reference in New Issue