terra/token_bridge: transfer with payload

Also rename terra token-bridge package so it's unique Otherwise cargo
can't find it externally, and confuses it with the solana one.
This commit is contained in:
Csongor Kiss 2022-04-13 12:30:22 +01:00 committed by Evan Gray
parent b23895684a
commit d8e7a5f93f
28 changed files with 2661 additions and 360 deletions

1167
terra/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,14 @@
[workspace]
members = ["contracts/cw20-wrapped", "contracts/wormhole", "contracts/token-bridge", "contracts/nft-bridge", "contracts/cw721-wrapped", "packages/cw721", "contracts/cw721-base"]
members = [
"contracts/cw20-wrapped",
"contracts/wormhole",
"contracts/token-bridge",
"contracts/nft-bridge",
"contracts/cw721-wrapped",
"packages/cw721",
"contracts/cw721-base",
"contracts/mock-bridge-integration",
]
[profile.release]
opt-level = 3

View File

@ -1,10 +1,10 @@
bridge_SOURCE=wormhole
token_bridge_SOURCE=token_bridge
token_bridge_SOURCE=token_bridge_terra
nft_bridge_SOURCE=nft_bridge
SOURCE_FILES=$(shell find . -name "*.rs" -or -name "*.lock" -or -name "*.toml" | grep -v target)
PACKAGES=$(shell find . -name "Cargo.toml" | grep -E 'packages|contracts' | cut -d/ -f3 | sed s/-/_/g)
PACKAGES=$(shell find . -name "Cargo.toml" | grep -E 'packages|contracts' | xargs cat | grep "name *=" | cut -d' ' -f3 | sed s/\"//g | sed s/-/_/g)
WASMS=$(patsubst %, artifacts/%.wasm, $(PACKAGES))
-include ../Makefile.help
@ -40,7 +40,7 @@ deploy/token_bridge: token_bridge-code-id-$(NETWORK).txt
## Deploy NFT bridge
deploy/nft_bridge: nft_bridge-code-id-$(NETWORK).txt
%-code-id-$(NETWORK).txt: check-network tools/node_modules payer-$(NETWORK).json
%-code-id-$(NETWORK).txt: check-network tools/node_modules payer-$(NETWORK).json artifacts
@echo "Deploying artifacts/$($*_SOURCE).wasm on $(NETWORK)"
@node tools/deploy_single.js \
--network $(NETWORK) \
@ -59,9 +59,15 @@ LocalTerra:
test/node_modules: test/package-lock.json
cd test && npm ci
.PHONY: unit-test
## Run unit tests
unit-test:
cargo test -p wormhole-bridge-terra
cargo test -p token-bridge-terra
.PHONY: test
## Run integration test
test: artifacts test/node_modules LocalTerra
## Run unit and integration tests
test: artifacts test/node_modules LocalTerra unit-test
@if pgrep terrad; then echo "Error: terrad already running. Stop it before running tests"; exit 1; fi
cd LocalTerra && docker compose up --detach
sleep 5

View File

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

View File

@ -0,0 +1,37 @@
[package]
name = "mock-bridge-integration"
version = "0.1.0"
edition = "2018"
description = "Mock Bridge Integration for Transfer w/ Payload"
[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 = "0.8.0"
cw20-base = { version = "0.8.0", features = ["library"] }
cw20-wrapped = { path = "../cw20-wrapped", features = ["library"] }
terraswap = "2.4.0"
thiserror = { version = "1.0.20" }
k256 = { version = "0.9.4", 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"
wormhole-bridge-terra = { path = "../wormhole", features = ["library"] }
token-bridge-terra = { path = "../token-bridge", features = ["library"] }
[dev-dependencies]
cosmwasm-vm = { version = "0.16.0", default-features = false }
serde_json = "1.0"

View File

@ -0,0 +1,120 @@
use cosmwasm_std::{
entry_point,
to_binary,
Binary,
CosmosMsg,
Deps,
DepsMut,
Env,
MessageInfo,
QueryRequest,
Reply,
Response,
StdError,
StdResult,
SubMsg,
WasmMsg,
WasmQuery,
};
use crate::{
msg::{
ExecuteMsg,
InstantiateMsg,
MigrateMsg,
QueryMsg,
},
state::{
Config,
config,
config_read,
},
};
use token_bridge_terra::{
msg::{
ExecuteMsg as TokenBridgeExecuteMsg,
QueryMsg as TokenBridgeQueryMessage,
},
msg::TransferInfoResponse,
};
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
Ok(Response::new())
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
let state = Config {
token_bridge_contract: msg.token_bridge_contract,
};
config(deps.storage).save(&state)?;
Ok(Response::default())
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult<Response> {
match msg {
ExecuteMsg::CompleteTransferWithPayload { data } => {
complete_transfer_with_payload(deps, env, info, &data)
},
}
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn reply(_deps: DepsMut, _env: Env, _msg: Reply) -> StdResult<Response> {
Ok(Response::default())
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(_deps: Deps, _env: Env, _msg: QueryMsg) -> StdResult<Binary> {
Err(StdError::generic_err("not implemented"))
}
fn complete_transfer_with_payload(
mut deps: DepsMut,
_env: Env,
info: MessageInfo,
data: &Binary,
) -> StdResult<Response> {
let cfg = config_read(deps.storage).load()?;
let messages = vec![SubMsg::reply_on_success(
CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: cfg.token_bridge_contract,
msg: to_binary(&TokenBridgeExecuteMsg::CompleteTransferWithPayload {
data: data.clone(), relayer: info.sender.to_string()
})?,
funds: vec![],
}),
1,
),];
let transfer_info = parse_transfer_vaa(deps.as_ref(), data)?;
Ok(Response::new()
.add_submessages(messages)
.add_attribute("action", "complete_transfer_with_payload")
.add_attribute("transfer_payload", Binary::from(transfer_info.payload).to_base64()))
}
fn parse_transfer_vaa(
deps: Deps,
data: &Binary,
) -> StdResult<TransferInfoResponse> {
let cfg = config_read(deps.storage).load()?;
let transfer_info: TransferInfoResponse = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
contract_addr: cfg.token_bridge_contract,
msg: to_binary(&TokenBridgeQueryMessage::TransferInfo {
vaa: data.clone(),
})?,
}))?;
Ok(transfer_info)
}

View File

@ -0,0 +1,6 @@
#[cfg(test)]
extern crate lazy_static;
pub mod contract;
pub mod msg;
pub mod state;

View File

@ -0,0 +1,31 @@
use cosmwasm_std::Binary;
use schemars::JsonSchema;
use serde::{
Deserialize,
Serialize,
};
type HumanAddr = String;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
pub token_bridge_contract: HumanAddr,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
CompleteTransferWithPayload {
data: Binary,
},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct MigrateMsg {}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
WrappedRegistry { chain: u16, address: Binary },
}

View File

@ -0,0 +1,31 @@
use schemars::JsonSchema;
use serde::{
Deserialize,
Serialize,
};
use cosmwasm_std::Storage;
use cosmwasm_storage::{
singleton,
singleton_read,
ReadonlySingleton,
Singleton,
};
type HumanAddr = String;
pub static CONFIG_KEY: &[u8] = b"config";
// Guardian set information
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Config {
pub token_bridge_contract: HumanAddr,
}
pub fn config(storage: &mut dyn Storage) -> Singleton<Config> {
singleton(storage, CONFIG_KEY)
}
pub fn config_read(storage: &dyn Storage) -> ReadonlySingleton<Config> {
singleton_read(storage, CONFIG_KEY)
}

View File

@ -1,5 +1,5 @@
[package]
name = "token-bridge"
name = "token-bridge-terra"
version = "0.1.0"
authors = ["Yuriy Savchenko <yuriy.savchenko@gmail.com>"]
edition = "2018"

View File

@ -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);
}

View File

@ -1,4 +1,54 @@
use crate::msg::WrappedRegistryResponse;
use cw20::{
BalanceResponse,
TokenInfoResponse,
};
use cw20_base::msg::{
ExecuteMsg as TokenMsg,
QueryMsg as TokenQuery,
};
use cw20_wrapped::msg::{
ExecuteMsg as WrappedMsg,
InitHook,
InstantiateMsg as WrappedInit,
QueryMsg as WrappedQuery,
WrappedAssetInfoResponse,
};
use sha3::{
Digest,
Keccak256,
};
use std::{
cmp::{
max,
min,
},
str::FromStr,
};
use terraswap::asset::{
Asset,
AssetInfo,
};
use wormhole::{
byte_utils::{
extend_address_to_32,
extend_string_to_32,
get_string_from_32,
ByteUtils,
},
error::ContractError,
msg::{
ExecuteMsg as WormholeExecuteMsg,
QueryMsg as WormholeQueryMsg,
},
state::{
vaa_archive_add,
vaa_archive_check,
GovernancePacket,
ParsedVAA,
},
};
use cosmwasm_std::{
coin,
entry_point,
@ -30,6 +80,8 @@ use crate::{
InstantiateMsg,
MigrateMsg,
QueryMsg,
TransferInfoResponse,
WrappedRegistryResponse,
},
state::{
bridge_contracts,
@ -53,64 +105,10 @@ use crate::{
TokenBridgeMessage,
TransferInfo,
TransferState,
TransferWithPayloadInfo,
UpgradeContract,
},
};
use wormhole::{
byte_utils::{
extend_address_to_32,
extend_string_to_32,
get_string_from_32,
ByteUtils,
},
error::ContractError,
};
use cw20_base::msg::{
ExecuteMsg as TokenMsg,
QueryMsg as TokenQuery,
};
use wormhole::msg::{
ExecuteMsg as WormholeExecuteMsg,
QueryMsg as WormholeQueryMsg,
};
use wormhole::state::{
vaa_archive_add,
vaa_archive_check,
GovernancePacket,
ParsedVAA,
};
use cw20::{
BalanceResponse,
TokenInfoResponse,
};
use cw20_wrapped::msg::{
ExecuteMsg as WrappedMsg,
InitHook,
InstantiateMsg as WrappedInit,
QueryMsg as WrappedQuery,
WrappedAssetInfoResponse,
};
use terraswap::asset::{
Asset,
AssetInfo,
};
use sha3::{
Digest,
Keccak256,
};
use std::{
cmp::{
max,
min,
},
str::FromStr,
};
type HumanAddr = String;
@ -119,6 +117,11 @@ const CHAIN_ID: u16 = 3;
const WRAPPED_ASSET_UPDATING: &str = "updating";
pub enum TransferType<A> {
WithoutPayload,
WithPayload { payload: A },
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
Ok(Response::new())
@ -134,7 +137,7 @@ pub fn instantiate(
// Save general wormhole info
let state = ConfigInfo {
gov_chain: msg.gov_chain,
gov_address: msg.gov_address.as_slice().to_vec(),
gov_address: msg.gov_address.into(),
wormhole_contract: msg.wormhole_contract,
wrapped_asset_code_id: msg.wrapped_asset_code_id,
};
@ -155,15 +158,33 @@ pub fn reply(deps: DepsMut, env: Env, _msg: Reply) -> StdResult<Response> {
// for why this is necessary.
wrapped_transfer_tmp(deps.storage).remove();
let mut info = TransferInfo::deserialize(&state.message)?;
let token_bridge_message = TokenBridgeMessage::deserialize(&state.message)?;
let (mut transfer_info, transfer_type) = match token_bridge_message.action {
Action::TRANSFER => {
let info = TransferInfo::deserialize(&token_bridge_message.payload)?;
Ok((info, TransferType::WithoutPayload))
}
Action::TRANSFER_WITH_PAYLOAD => {
let info = TransferWithPayloadInfo::deserialize(&token_bridge_message.payload)?;
Ok((
info.transfer_info,
TransferType::WithPayload {
payload: info.payload,
},
))
}
_ => Err(StdError::generic_err("Unreachable")),
}?;
// Fetch CW20 Balance post-transfer.
let new_balance: BalanceResponse = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
contract_addr: state.token_address.clone(),
msg: to_binary(&TokenQuery::Balance {
address: env.contract.address.to_string(),
})?,
}))?;
let new_balance: BalanceResponse =
deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
contract_addr: state.token_address.clone(),
msg: to_binary(&TokenQuery::Balance {
address: env.contract.address.to_string(),
})?,
}))?;
// Actual amount should be the difference in balance of the CW20 account in question to account
// for fee tokens.
@ -172,16 +193,26 @@ pub fn reply(deps: DepsMut, env: Env, _msg: Reply) -> StdResult<Response> {
let real_amount = real_amount / multiplier;
// If the fee is too large the user would receive nothing.
if info.fee.1 > real_amount.u128() {
if transfer_info.fee.1 > real_amount.u128() {
return Err(StdError::generic_err("fee greater than sent amount"));
}
// Update Wormhole message to correct amount.
info.amount.1 = real_amount.u128();
transfer_info.amount.1 = real_amount.u128();
let token_bridge_message = TokenBridgeMessage {
action: Action::TRANSFER,
payload: info.serialize(),
let token_bridge_message = match transfer_type {
TransferType::WithoutPayload => TokenBridgeMessage {
action: Action::TRANSFER,
payload: transfer_info.serialize(),
},
TransferType::WithPayload { payload } => TokenBridgeMessage {
action: Action::TRANSFER_WITH_PAYLOAD,
payload: TransferWithPayloadInfo {
transfer_info,
payload,
}
.serialize(),
},
};
// Post Wormhole Message
@ -194,7 +225,7 @@ pub fn reply(deps: DepsMut, env: Env, _msg: Reply) -> StdResult<Response> {
})?,
});
send_native(deps.storage, &state.token_canonical, info.amount.1.into())?;
send_native(deps.storage, &state.token_canonical, real_amount)?;
Ok(Response::default()
.add_message(message)
.add_attribute("action", "reply_handler"))
@ -214,10 +245,10 @@ pub fn coins_after_tax(deps: DepsMut, coins: Vec<Coin>) -> StdResult<Vec<Coin>>
Ok(res)
}
pub fn parse_vaa(deps: DepsMut, block_time: u64, data: &Binary) -> StdResult<ParsedVAA> {
fn parse_vaa(deps: Deps, 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(),
contract_addr: cfg.wormhole_contract,
msg: to_binary(&WormholeQueryMsg::VerifyVAA {
vaa: data.clone(),
block_time,
@ -244,8 +275,29 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S
info,
asset,
recipient_chain,
recipient.as_slice().to_vec(),
recipient.into(),
fee,
TransferType::WithoutPayload,
nonce,
),
ExecuteMsg::InitiateTransferWithPayload {
asset,
recipient_chain,
recipient,
fee,
payload,
nonce,
} => handle_initiate_transfer(
deps,
env,
info,
asset,
recipient_chain,
recipient.into(),
fee,
TransferType::WithPayload {
payload: payload.into(),
},
nonce,
),
ExecuteMsg::DepositTokens {} => deposit_tokens(deps, env, info),
@ -254,6 +306,9 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S
ExecuteMsg::CreateAssetMeta { asset_info, nonce } => {
handle_create_asset_meta(deps, env, info, asset_info, nonce)
}
ExecuteMsg::CompleteTransferWithPayload { data, relayer } => {
handle_complete_transfer_with_payload(deps, env, info, &data, &relayer)
}
}
}
@ -500,15 +555,16 @@ fn handle_create_asset_meta_native_token(
.add_attribute("meta.block_time", env.block.time.seconds().to_string()))
}
fn submit_vaa(
mut deps: DepsMut,
fn handle_complete_transfer_with_payload(
deps: DepsMut,
env: Env,
info: MessageInfo,
data: &Binary,
relayer_address: &HumanAddr,
) -> StdResult<Response> {
let state = config_read(deps.storage).load()?;
let vaa = parse_vaa(deps.branch(), env.block.time.seconds(), data)?;
let vaa = parse_vaa(deps.as_ref(), env.block.time.seconds(), data)?;
let data = vaa.payload;
if vaa_archive_check(deps.storage, vaa.hash.as_slice()) {
@ -517,21 +573,64 @@ fn submit_vaa(
vaa_archive_add(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 {
if is_governance_emitter(&state, vaa.emitter_chain, &vaa.emitter_address) {
return ContractError::InvalidVAAAction.std_err();
}
let message = TokenBridgeMessage::deserialize(&data)?;
match message.action {
Action::TRANSFER_WITH_PAYLOAD => handle_complete_transfer(
deps,
env,
info,
vaa.emitter_chain,
vaa.emitter_address,
TransferType::WithPayload { payload: () },
&message.payload,
relayer_address,
),
_ => ContractError::InvalidVAAAction.std_err(),
}
}
fn submit_vaa(
deps: DepsMut,
env: Env,
info: MessageInfo,
data: &Binary,
) -> StdResult<Response> {
let state = config_read(deps.storage).load()?;
let vaa = parse_vaa(deps.as_ref(), env.block.time.seconds(), data)?;
let data = vaa.payload;
if vaa_archive_check(deps.storage, vaa.hash.as_slice()) {
return ContractError::VaaAlreadyExecuted.std_err();
}
vaa_archive_add(deps.storage, vaa.hash.as_slice())?;
// check if vaa is from governance
if is_governance_emitter(&state, vaa.emitter_chain, &vaa.emitter_address) {
return handle_governance_payload(deps, env, &data);
}
let message = TokenBridgeMessage::deserialize(&data)?;
match message.action {
Action::TRANSFER => handle_complete_transfer(
deps,
env,
info,
vaa.emitter_chain,
vaa.emitter_address,
&message.payload,
),
Action::TRANSFER => {
let sender = info.sender.to_string();
handle_complete_transfer(
deps,
env,
info,
vaa.emitter_chain,
vaa.emitter_address,
TransferType::WithoutPayload,
&message.payload,
&sender,
)
}
Action::ATTEST_META => handle_attest_meta(
deps,
env,
@ -604,7 +703,9 @@ fn handle_complete_transfer(
info: MessageInfo,
emitter_chain: u16,
emitter_address: Vec<u8>,
transfer_type: TransferType<()>,
data: &Vec<u8>,
relayer_address: &HumanAddr,
) -> StdResult<Response> {
let transfer_info = TransferInfo::deserialize(&data)?;
match transfer_info.token_address.as_slice()[0] {
@ -614,9 +715,20 @@ fn handle_complete_transfer(
info,
emitter_chain,
emitter_address,
transfer_type,
data,
relayer_address,
),
_ => handle_complete_transfer_token(
deps,
env,
info,
emitter_chain,
emitter_address,
transfer_type,
data,
relayer_address,
),
_ => handle_complete_transfer_token(deps, env, info, emitter_chain, emitter_address, data),
}
}
@ -626,9 +738,17 @@ fn handle_complete_transfer_token(
info: MessageInfo,
emitter_chain: u16,
emitter_address: Vec<u8>,
transfer_type: TransferType<()>,
data: &Vec<u8>,
relayer_address: &HumanAddr,
) -> StdResult<Response> {
let transfer_info = TransferInfo::deserialize(&data)?;
let transfer_info = match transfer_type {
TransferType::WithoutPayload => TransferInfo::deserialize(&data)?,
TransferType::WithPayload { payload: _ } => {
TransferWithPayloadInfo::deserialize(&data)?.transfer_info
}
};
let expected_contract =
bridge_contracts_read(deps.storage).load(&emitter_chain.to_be_bytes())?;
@ -645,6 +765,15 @@ fn handle_complete_transfer_token(
let token_chain = transfer_info.token_chain;
let target_address = (&transfer_info.recipient.as_slice()).get_address(0);
let recipient = deps.api.addr_humanize(&target_address)?;
if let TransferType::WithPayload { payload: _ } = transfer_type {
if recipient != info.sender {
return Err(StdError::generic_err(
"transfers with payload can only be redeemed by the recipient",
));
}
};
let (not_supported_amount, mut amount) = transfer_info.amount;
let (not_supported_fee, mut fee) = transfer_info.fee;
@ -666,11 +795,6 @@ fn handle_complete_transfer_token(
return if let Some(contract_addr) = contract_addr {
// Asset already deployed, just mint
let recipient = deps
.api
.addr_humanize(&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 {
@ -683,7 +807,7 @@ fn handle_complete_transfer_token(
messages.push(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: contract_addr.clone(),
msg: to_binary(&WrappedMsg::Mint {
recipient: info.sender.to_string(),
recipient: relayer_address.to_string(),
amount: Uint128::from(fee),
})?,
funds: vec![],
@ -695,14 +819,15 @@ fn handle_complete_transfer_token(
.add_attribute("action", "complete_transfer_wrapped")
.add_attribute("contract", contract_addr)
.add_attribute("recipient", recipient)
.add_attribute("amount", amount.to_string()))
.add_attribute("amount", amount.to_string())
.add_attribute("relayer", relayer_address)
.add_attribute("fee", fee.to_string()))
} 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.addr_humanize(&target_address)?;
let contract_addr = deps.api.addr_humanize(&token_address)?;
// note -- here the amount is the amount the recipient will receive;
@ -734,7 +859,7 @@ fn handle_complete_transfer_token(
messages.push(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: contract_addr.to_string(),
msg: to_binary(&TokenMsg::Transfer {
recipient: info.sender.to_string(),
recipient: relayer_address.to_string(),
amount: Uint128::from(fee),
})?,
funds: vec![],
@ -746,7 +871,9 @@ fn handle_complete_transfer_token(
.add_attribute("action", "complete_transfer_native")
.add_attribute("recipient", recipient)
.add_attribute("contract", contract_addr)
.add_attribute("amount", amount.to_string()))
.add_attribute("amount", amount.to_string())
.add_attribute("relayer", relayer_address)
.add_attribute("fee", fee.to_string()))
}
}
@ -756,9 +883,16 @@ fn handle_complete_transfer_token_native(
info: MessageInfo,
emitter_chain: u16,
emitter_address: Vec<u8>,
transfer_type: TransferType<()>,
data: &Vec<u8>,
relayer_address: &HumanAddr,
) -> StdResult<Response> {
let transfer_info = TransferInfo::deserialize(&data)?;
let transfer_info = match transfer_type {
TransferType::WithoutPayload => TransferInfo::deserialize(&data)?,
TransferType::WithPayload { payload: () } => {
TransferWithPayloadInfo::deserialize(&data)?.transfer_info
}
};
let expected_contract =
bridge_contracts_read(deps.storage).load(&emitter_chain.to_be_bytes())?;
@ -775,6 +909,15 @@ fn handle_complete_transfer_token_native(
}
let target_address = (&transfer_info.recipient.as_slice()).get_address(0);
let recipient = deps.api.addr_humanize(&target_address)?;
if let TransferType::WithPayload { payload: _ } = transfer_type {
if recipient != info.sender {
return Err(StdError::generic_err(
"transfers with payload can only be redeemed by the recipient",
));
}
};
let (not_supported_amount, mut amount) = transfer_info.amount;
let (not_supported_fee, fee) = transfer_info.fee;
@ -797,7 +940,6 @@ fn handle_complete_transfer_token_native(
// note -- here the amount is the amount the recipient will receive;
// amount + fee is the total sent
let recipient = deps.api.addr_humanize(&target_address)?;
let token_address = (&*token_address).get_address(0);
receive_native(deps.storage, &token_address, Uint128::new(amount + fee))?;
@ -808,7 +950,7 @@ fn handle_complete_transfer_token_native(
if fee != 0 {
messages.push(CosmosMsg::Bank(BankMsg::Send {
to_address: info.sender.to_string(),
to_address: relayer_address.to_string(),
amount: coins_after_tax(deps, vec![coin(fee, &denom)])?,
}));
}
@ -818,7 +960,9 @@ fn handle_complete_transfer_token_native(
.add_attribute("action", "complete_transfer_terra_native")
.add_attribute("recipient", recipient)
.add_attribute("denom", denom)
.add_attribute("amount", amount.to_string()))
.add_attribute("amount", amount.to_string())
.add_attribute("relayer", relayer_address)
.add_attribute("fee", fee.to_string()))
}
fn handle_initiate_transfer(
@ -829,6 +973,7 @@ fn handle_initiate_transfer(
recipient_chain: u16,
recipient: Vec<u8>,
fee: Uint128,
transfer_type: TransferType<Vec<u8>>,
nonce: u32,
) -> StdResult<Response> {
match asset.info {
@ -841,6 +986,7 @@ fn handle_initiate_transfer(
recipient_chain,
recipient,
fee,
transfer_type,
nonce,
),
AssetInfo::NativeToken { ref denom } => handle_initiate_transfer_native_token(
@ -852,6 +998,7 @@ fn handle_initiate_transfer(
recipient_chain,
recipient,
fee,
transfer_type,
nonce,
),
}
@ -866,6 +1013,7 @@ fn handle_initiate_transfer_token(
recipient_chain: u16,
recipient: Vec<u8>,
mut fee: Uint128,
transfer_type: TransferType<Vec<u8>>,
nonce: u32,
) -> StdResult<Response> {
if recipient_chain == CHAIN_ID {
@ -907,7 +1055,7 @@ fn handle_initiate_transfer_token(
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();
asset_address = wrapped_token_info.asset_address.into();
let transfer_info = TransferInfo {
token_chain: asset_chain,
@ -918,9 +1066,19 @@ fn handle_initiate_transfer_token(
fee: (0, fee.u128()),
};
let token_bridge_message = TokenBridgeMessage {
action: Action::TRANSFER,
payload: transfer_info.serialize(),
let token_bridge_message: TokenBridgeMessage = match transfer_type {
TransferType::WithoutPayload => TokenBridgeMessage {
action: Action::TRANSFER,
payload: transfer_info.serialize(),
},
TransferType::WithPayload { payload } => TokenBridgeMessage {
action: Action::TRANSFER_WITH_PAYLOAD,
payload: TransferWithPayloadInfo {
transfer_info,
payload,
}
.serialize(),
},
};
messages.push(CosmosMsg::Wasm(WasmMsg::Execute {
@ -1006,13 +1164,29 @@ fn handle_initiate_transfer_token(
// that there's no execution in progress. The reply handler takes
// care of clearing out this temporary storage when done.
assert!(wrapped_transfer_tmp(deps.storage).load().is_err());
let token_bridge_message: TokenBridgeMessage = match transfer_type {
TransferType::WithoutPayload => TokenBridgeMessage {
action: Action::TRANSFER,
payload: transfer_info.serialize(),
},
TransferType::WithPayload { payload } => TokenBridgeMessage {
action: Action::TRANSFER_WITH_PAYLOAD,
payload: TransferWithPayloadInfo {
transfer_info,
payload,
}
.serialize(),
},
};
// Wrap up state to be captured by the submessage reply.
wrapped_transfer_tmp(deps.storage).save(&TransferState {
previous_balance: balance.balance.to_string(),
account: info.sender.to_string(),
token_address: asset,
token_canonical: asset_canonical.clone(),
message: transfer_info.serialize(),
message: token_bridge_message.serialize(),
multiplier: Uint128::new(multiplier).to_string(),
nonce,
})?;
@ -1056,6 +1230,7 @@ fn handle_initiate_transfer_native_token(
recipient_chain: u16,
recipient: Vec<u8>,
fee: Uint128,
transfer_type: TransferType<Vec<u8>>,
nonce: u32,
) -> StdResult<Response> {
if recipient_chain == CHAIN_ID {
@ -1097,9 +1272,19 @@ fn handle_initiate_transfer_native_token(
fee: (0, fee.u128()),
};
let token_bridge_message = TokenBridgeMessage {
action: Action::TRANSFER,
payload: transfer_info.serialize(),
let token_bridge_message: TokenBridgeMessage = match transfer_type {
TransferType::WithoutPayload => TokenBridgeMessage {
action: Action::TRANSFER,
payload: transfer_info.serialize(),
},
TransferType::WithPayload { payload } => TokenBridgeMessage {
action: Action::TRANSFER_WITH_PAYLOAD,
payload: TransferWithPayloadInfo {
transfer_info,
payload,
}
.serialize(),
},
};
let sender = deps.api.addr_canonicalize(&info.sender.as_str())?;
@ -1128,11 +1313,12 @@ fn handle_initiate_transfer_native_token(
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::WrappedRegistry { chain, address } => {
to_binary(&query_wrapped_registry(deps, chain, address.as_slice())?)
}
QueryMsg::TransferInfo { vaa } => to_binary(&query_transfer_info(deps, env, &vaa)?),
}
}
@ -1149,9 +1335,41 @@ pub fn query_wrapped_registry(
}
}
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());
fn query_transfer_info(deps: Deps, env: Env, vaa: &Binary) -> StdResult<TransferInfoResponse> {
let cfg = config_read(deps.storage).load()?;
let parsed = parse_vaa(deps, env.block.time.seconds(), vaa)?;
let data = parsed.payload;
// check if vaa is from governance
if is_governance_emitter(&cfg, parsed.emitter_chain, &parsed.emitter_address) {
return ContractError::InvalidVAAAction.std_err();
}
let message = TokenBridgeMessage::deserialize(&data)?;
match message.action {
Action::ATTEST_META => ContractError::InvalidVAAAction.std_err(),
_ => {
let info = TransferWithPayloadInfo::deserialize(&message.payload)?;
let core = info.transfer_info;
Ok(TransferInfoResponse {
amount: core.amount.1.into(),
token_address: core.token_address,
token_chain: core.token_chain,
recipient: core.recipient,
recipient_chain: core.recipient_chain,
fee: core.fee.1.into(),
payload: info.payload,
})
}
}
}
pub fn build_asset_id(chain: u16, address: &[u8]) -> Vec<u8> {
let chain = &chain.to_be_bytes();
let mut asset_id = Vec::with_capacity(chain.len() + address.len());
asset_id.extend_from_slice(chain);
asset_id.extend_from_slice(address);
let mut hasher = Keccak256::new();
@ -1160,52 +1378,15 @@ fn build_asset_id(chain: u16, address: &[u8]) -> Vec<u8> {
}
// Produce a 20 byte asset "address" from a native terra denom.
fn build_native_id(denom: &str) -> Vec<u8> {
let mut asset_address: Vec<u8> = denom.clone().as_bytes().to_vec();
asset_address.reverse();
asset_address.extend(vec![0u8; 20 - denom.len()]);
asset_address.reverse();
assert_eq!(asset_address.len(), 20);
pub fn build_native_id(denom: &str) -> Vec<u8> {
let n = denom.len();
assert!(n < 20);
let mut asset_address = Vec::with_capacity(20);
asset_address.resize(20 - n, 0u8);
asset_address.extend_from_slice(denom.as_bytes());
asset_address
}
#[cfg(test)]
mod tests {
use cosmwasm_std::{
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(())
}
fn is_governance_emitter(cfg: &ConfigInfo, emitter_chain: u16, emitter_address: &Vec<u8>) -> bool {
cfg.gov_chain == emitter_chain && cfg.gov_address == emitter_address.clone()
}

View File

@ -4,3 +4,6 @@ extern crate lazy_static;
pub mod contract;
pub mod msg;
pub mod state;
#[cfg(test)]
mod testing;

View File

@ -44,6 +44,15 @@ pub enum ExecuteMsg {
nonce: u32,
},
InitiateTransferWithPayload {
asset: Asset,
recipient_chain: u16,
recipient: Binary,
fee: Uint128,
payload: Binary,
nonce: u32,
},
SubmitVaa {
data: Binary,
},
@ -52,6 +61,11 @@ pub enum ExecuteMsg {
asset_info: AssetInfo,
nonce: u32,
},
CompleteTransferWithPayload {
data: Binary,
relayer: HumanAddr,
},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
@ -62,6 +76,7 @@ pub struct MigrateMsg {}
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
WrappedRegistry { chain: u16, address: Binary },
TransferInfo { vaa: Binary },
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
@ -72,6 +87,12 @@ pub struct WrappedRegistryResponse {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WormholeQueryMsg {
VerifyVAA { vaa: Binary, block_time: u64 },
pub struct TransferInfoResponse {
pub amount: Uint128,
pub token_address: Vec<u8>,
pub token_chain: u16,
pub recipient: Vec<u8>,
pub recipient_chain: u16,
pub fee: Uint128,
pub payload: Vec<u8>,
}

View File

@ -147,6 +147,7 @@ pub struct Action;
impl Action {
pub const TRANSFER: u8 = 1;
pub const ATTEST_META: u8 = 2;
pub const TRANSFER_WITH_PAYLOAD: u8 = 3;
}
// 0 u8 action
@ -225,6 +226,38 @@ impl TransferInfo {
}
}
// 0 u256 amount
// 32 [u8; 32] token_address
// 64 u16 token_chain
// 66 [u8; 32] recipient
// 98 u16 recipient_chain
// 100 u256 fee
// 132 [u8] payload
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct TransferWithPayloadInfo {
pub transfer_info: TransferInfo,
pub payload: Vec<u8>,
}
impl TransferWithPayloadInfo {
pub fn deserialize(data: &Vec<u8>) -> StdResult<Self> {
let transfer_info = TransferInfo::deserialize(data)?;
let payload = TransferWithPayloadInfo::get_payload(data);
Ok(TransferWithPayloadInfo {
transfer_info,
payload,
})
}
pub fn serialize(&self) -> Vec<u8> {
[self.transfer_info.serialize(), self.payload.clone()].concat()
}
pub fn get_payload(data: &Vec<u8>) -> Vec<u8> {
return data[132..].to_vec();
}
}
// 0 [32]uint8 TokenAddress
// 32 uint16 TokenChain
// 34 uint8 Decimals

View File

@ -0,0 +1 @@
mod tests;

View File

@ -0,0 +1,197 @@
use cosmwasm_std::{
Binary,
StdResult,
};
use wormhole::state::ParsedVAA;
use crate::{
contract::{
build_asset_id,
build_native_id,
},
state::{
Action,
TokenBridgeMessage,
TransferInfo,
TransferWithPayloadInfo,
},
};
#[test]
fn binary_check() -> 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: Vec<u8> = b.into();
assert_eq!(x, y);
Ok(())
}
#[test]
fn build_native_and_asset_ids() -> StdResult<()> {
let denom = "uusd";
let native_id = build_native_id(denom);
let expected_native_id = vec![
0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 117u8,
117u8, 115u8, 100u8,
];
assert_eq!(&native_id, &expected_native_id, "native_id != expected");
// weth
let chain = 2u16;
let token_address = "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2";
let token_address = hex::decode(token_address).unwrap();
let asset_id = build_asset_id(chain, token_address.as_slice());
let expected_asset_id = vec![
171u8, 106u8, 233u8, 80u8, 14u8, 139u8, 124u8, 78u8, 181u8, 77u8, 142u8, 76u8, 109u8, 81u8,
55u8, 100u8, 139u8, 159u8, 42u8, 85u8, 172u8, 234u8, 0u8, 114u8, 11u8, 82u8, 40u8, 40u8,
50u8, 73u8, 211u8, 135u8,
];
assert_eq!(&asset_id, &expected_asset_id, "asset_id != expected");
Ok(())
}
#[test]
fn deserialize_transfer_vaa() -> StdResult<()> {
let signed_vaa = "\
010000000001003f3179d5bb17b6f2ecc13741ca3f78d922043e99e09975e390\
4332d2418bb3f16d7ac93ca8401f8bed1cf9827bc806ecf7c5a283340f033bf4\
72724abf1d274f00000000000000000000010000000000000000000000000000\
00000000000000000000000000000000ffff0000000000000000000100000000\
00000000000000000000000000000000000000000000000005f5e10001000000\
0000000000000000000000000000000000000000000000007575736400030000\
00000000000000000000f7f7dde848e7450a029cd0a9bd9bdae4b5147db30003\
00000000000000000000000000000000000000000000000000000000000f4240";
let signed_vaa = hex::decode(signed_vaa).unwrap();
let parsed = ParsedVAA::deserialize(signed_vaa.as_slice())?;
let message = TokenBridgeMessage::deserialize(&parsed.payload)?;
assert_eq!(
message.action,
Action::TRANSFER,
"message.action != expected"
);
let info = TransferInfo::deserialize(&message.payload)?;
let amount = (0u128, 100_000_000u128);
assert_eq!(info.amount, amount, "info.amount != expected");
let token_address = "0100000000000000000000000000000000000000000000000000000075757364";
let token_address = hex::decode(token_address).unwrap();
assert_eq!(
info.token_address, token_address,
"info.token_address != expected"
);
let token_chain = 3u16;
assert_eq!(
info.token_chain, token_chain,
"info.token_chain != expected"
);
let recipient = "000000000000000000000000f7f7dde848e7450a029cd0a9bd9bdae4b5147db3";
let recipient = hex::decode(recipient).unwrap();
assert_eq!(info.recipient, recipient, "info.recipient != expected");
let recipient_chain = 3u16;
assert_eq!(
info.recipient_chain, recipient_chain,
"info.recipient_chain != expected"
);
let fee = (0u128, 1_000_000u128);
assert_eq!(info.fee, fee, "info.fee != expected");
Ok(())
}
#[test]
fn deserialize_transfer_with_payload_vaa() -> StdResult<()> {
let signed_vaa = "\
010000000001002b0e392ebe370e718b91dcafbba21094efd8e7f1f12e28bd90\
a178b4dfbbc708675152a3cd2edd20e8e018600026b73b6c6cbf02622903409e\
8b48ab7fa30ef001000000010000000100010000000000000000000000000000\
00000000000000000000000000000000ffff0000000000000002000300000000\
00000000000000000000000000000000000000000000000005f5e10001000000\
0000000000000000000000000000000000000000000000007575736400030000\
000000000000000000008cec800d24df11e556e708461c98122df4a2c3b10003\
00000000000000000000000000000000000000000000000000000000000f4240\
416c6c20796f75722062617365206172652062656c6f6e6720746f207573";
let signed_vaa = hex::decode(signed_vaa).unwrap();
let parsed = ParsedVAA::deserialize(signed_vaa.as_slice())?;
let message = TokenBridgeMessage::deserialize(&parsed.payload)?;
assert_eq!(
message.action,
Action::TRANSFER_WITH_PAYLOAD,
"message.action != expected"
);
let info_with_payload = TransferWithPayloadInfo::deserialize(&message.payload)?;
let info = info_with_payload.transfer_info;
let amount = (0u128, 100_000_000u128);
assert_eq!(info.amount, amount, "info.amount != expected");
let token_address = "0100000000000000000000000000000000000000000000000000000075757364";
let token_address = hex::decode(token_address).unwrap();
assert_eq!(
info.token_address, token_address,
"info.token_address != expected"
);
let token_chain = 3u16;
assert_eq!(
info.token_chain, token_chain,
"info.token_chain != expected"
);
let recipient = "0000000000000000000000008cec800d24df11e556e708461c98122df4a2c3b1";
let recipient = hex::decode(recipient).unwrap();
assert_eq!(info.recipient, recipient, "info.recipient != expected");
let recipient_chain = 3u16;
assert_eq!(
info.recipient_chain, recipient_chain,
"info.recipient_chain != expected"
);
let fee = (0u128, 1_000_000u128);
assert_eq!(info.fee, fee, "info.fee != expected");
let transfer_payload = "All your base are belong to us";
let transfer_payload = transfer_payload.as_bytes();
assert_eq!(
info_with_payload.payload.as_slice(),
transfer_payload,
"info.payload != expected"
);
Ok(())
}

View File

@ -5,3 +5,6 @@ pub mod msg;
pub mod state;
pub use crate::error::ContractError;
#[cfg(test)]
mod testing;

View File

@ -386,76 +386,3 @@ impl TransferFee {
}
}
#[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 body = &x[ParsedVAA::HEADER_LEN + ParsedVAA::SIGNATURE_LEN..];
let mut hasher = Keccak256::new();
hasher.update(body);
let hash = hasher.finalize();
// Rehash the hash
let mut hasher = Keccak256::new();
hasher.update(hash);
let hash = hasher.finalize().to_vec();
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,
}
);
}
}

View File

@ -0,0 +1 @@
mod tests;

View File

@ -0,0 +1,162 @@
use cosmwasm_std::StdResult;
use crate::state::{GuardianAddress, GuardianSetInfo, ParsedVAA};
#[test]
fn quardian_set_quorum() {
let num_guardians_trials: Vec<usize> = vec![
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 20, 25, 100,
];
let expected_quorums: Vec<usize> = vec![
1, 2, 3, 3, 4, 5, 5, 6, 7, 7, 8, 9, 14, 17, 67,
];
let make_guardian_set = |n: usize| -> GuardianSetInfo {
let mut addresses = Vec::with_capacity(n);
for _ in 0..n {
addresses.push(
GuardianAddress {
bytes: Vec::new().into(),
}
);
}
GuardianSetInfo {
addresses,
expiration_time: 0,
}
};
for (i, &num_guardians) in num_guardians_trials.iter().enumerate() {
let quorum = make_guardian_set(num_guardians).quorum();
assert_eq!(quorum, expected_quorums[i], "quorum != expected");
}
}
#[test]
fn deserialize_round_1() -> StdResult<()> {
let signed_vaa = "\
080000000901007bfa71192f886ab6819fa4862e34b4d178962958d9b2e3d943\
7338c9e5fde1443b809d2886eaa69e0f0158ea517675d96243c9209c3fe1d94d\
5b19866654c6980000000b150000000500020001020304000000000000000000\
000000000000000000000000000000000000000000000000000a0261626364";
let signed_vaa = hex::decode(signed_vaa).unwrap();
let parsed = ParsedVAA::deserialize(signed_vaa.as_slice())?;
let version = 8u8;
assert_eq!(parsed.version, version, "parsed.version != expected");
let guardian_set_index = 9u32;
assert_eq!(parsed.guardian_set_index, guardian_set_index, "parsed.guardian_set_index != expected");
let timestamp = 2837u32;
assert_eq!(parsed.timestamp, timestamp, "parsed.timestamp != expected");
let nonce = 5u32;
assert_eq!(parsed.nonce, nonce, "parsed.nonce != expected");
let len_signers = 1u8;
assert_eq!(parsed.len_signers, len_signers, "parsed.len_signers != expected");
let emitter_chain = 2u16;
assert_eq!(parsed.emitter_chain, emitter_chain, "parsed.emitter_chain != expected");
let emitter_address = "0001020304000000000000000000000000000000000000000000000000000000";
let emitter_address = hex::decode(emitter_address).unwrap();
assert_eq!(parsed.emitter_address, emitter_address, "parsed.emitter_address != expected");
let sequence = 10u64;
assert_eq!(parsed.sequence, sequence, "parsed.sequence != expected");
let consistency_level = 2u8;
assert_eq!(parsed.consistency_level, consistency_level, "parsed.consistency_level != expected");
let payload = vec![97u8, 98u8, 99u8, 100u8];
assert_eq!(parsed.payload, payload, "parsed.payload != expected");
let hash = vec![
164u8, 44u8, 82u8, 103u8, 33u8, 170u8, 183u8, 178u8,
188u8, 204u8, 35u8, 53u8, 78u8, 148u8, 160u8, 153u8,
122u8, 252u8, 84u8, 211u8, 26u8, 204u8, 128u8, 215u8,
37u8, 232u8, 222u8, 186u8, 222u8, 186u8, 98u8, 94u8,
];
assert_eq!(parsed.hash, hash, "parsed.hash != expected");
Ok(())
}
#[test]
fn deserialize_round_2() -> StdResult<()> {
let signed_vaa = "\
010000000001003f3179d5bb17b6f2ecc13741ca3f78d922043e99e09975e390\
4332d2418bb3f16d7ac93ca8401f8bed1cf9827bc806ecf7c5a283340f033bf4\
72724abf1d274f00000000000000000000010000000000000000000000000000\
00000000000000000000000000000000ffff0000000000000000000100000000\
00000000000000000000000000000000000000000000000005f5e10001000000\
0000000000000000000000000000000000000000000000007575736400030000\
00000000000000000000f7f7dde848e7450a029cd0a9bd9bdae4b5147db30003\
00000000000000000000000000000000000000000000000000000000000f4240";
let signed_vaa = hex::decode(signed_vaa).unwrap();
let parsed = ParsedVAA::deserialize(signed_vaa.as_slice())?;
let version = 1u8;
assert_eq!(parsed.version, version, "parsed.version != expected");
let guardian_set_index = 0u32;
assert_eq!(parsed.guardian_set_index, guardian_set_index, "parsed.guardian_set_index != expected");
let timestamp = 0u32;
assert_eq!(parsed.timestamp, timestamp, "parsed.timestamp != expected");
let nonce = 0u32;
assert_eq!(parsed.nonce, nonce, "parsed.nonce != expected");
let len_signers = 1u8;
assert_eq!(parsed.len_signers, len_signers, "parsed.len_signers != expected");
let emitter_chain = 1u16;
assert_eq!(parsed.emitter_chain, emitter_chain, "parsed.emitter_chain != expected");
let emitter_address = "000000000000000000000000000000000000000000000000000000000000ffff";
let emitter_address = hex::decode(emitter_address).unwrap();
assert_eq!(parsed.emitter_address, emitter_address, "parsed.emitter_address != expected");
let sequence = 0u64;
assert_eq!(parsed.sequence, sequence, "parsed.sequence != expected");
let consistency_level = 0u8;
assert_eq!(parsed.consistency_level, consistency_level, "parsed.consistency_level != expected");
let payload = vec![
1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8,
0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8,
0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8,
0u8, 0u8, 0u8, 0u8, 0u8, 5u8, 245u8, 225u8,
0u8, 1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8,
0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8,
0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8,
0u8, 0u8, 0u8, 0u8, 0u8, 117u8, 117u8, 115u8,
100u8, 0u8, 3u8, 0u8, 0u8, 0u8, 0u8, 0u8,
0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 247u8,
247u8, 221u8, 232u8, 72u8, 231u8, 69u8, 10u8, 2u8,
156u8, 208u8, 169u8, 189u8, 155u8, 218u8, 228u8, 181u8,
20u8, 125u8, 179u8, 0u8, 3u8, 0u8, 0u8, 0u8,
0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8,
0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8,
0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8,
0u8, 0u8, 15u8, 66u8, 64u8,
];
assert_eq!(parsed.payload, payload, "parsed.payload != expected");
let hash = vec![
114u8, 108u8, 111u8, 78u8, 204u8, 83u8, 150u8, 170u8,
240u8, 15u8, 193u8, 176u8, 165u8, 87u8, 174u8, 230u8,
94u8, 222u8, 106u8, 206u8, 179u8, 203u8, 193u8, 187u8,
1u8, 148u8, 17u8, 40u8, 248u8, 214u8, 147u8, 68u8,
];
assert_eq!(parsed.hash, hash, "parsed.hash != expected");
Ok(())
}

1
terra/test/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
lib

View File

@ -30,4 +30,4 @@ npm run test
These tests are built using Jest and is meant to be structured very similarly to the [ethereum unit tests](../../ethereum), which requires running a local node via ganache before _truffle_ can run any of the testing scripts in the [test directory](../../ethereum/test).
**Currently the only test that exists is for the token bridge's transfer.**
**Currently the only test that exists is for the token bridge's transfer and transfer with payload.**

View File

@ -1,15 +1,19 @@
import { describe, expect, jest, test } from "@jest/globals";
import { Bech32, toHex } from "@cosmjs/encoding";
import {
getNativeBalance,
makeProviderAndWallet,
transactWithoutMemo,
} from "../helpers/client";
import { storeCode, deploy } from "../instantiate";
import { Int, MsgExecuteContract } from "@terra-money/terra.js";
import { makeProviderAndWallet, transactWithoutMemo } from "../helpers/client";
import {
makeGovernanceVaaPayload,
makeTransferVaaPayload,
signAndEncodeVaa,
TEST_SIGNER_PKS,
} from "../helpers/vaa";
import { storeCode, deploy } from "../instantiate";
import { computeGasPaid, parseEventsFromLog } from "../helpers/receipt";
jest.setTimeout(60000);
@ -24,7 +28,9 @@ const CONSISTENCY_LEVEL = 0;
const WASM_WORMHOLE = "../artifacts/wormhole.wasm";
const WASM_WRAPPED_ASSET = "../artifacts/cw20_wrapped.wasm";
const WASM_TOKEN_BRIDGE = "../artifacts/token_bridge.wasm";
const WASM_TOKEN_BRIDGE = "../artifacts/token_bridge_terra.wasm";
const WASM_MOCK_BRIDGE_INTEGRATION =
"../artifacts/mock_bridge_integration.wasm";
// global map of contract addresses for all tests
const contracts = new Map<string, string>();
@ -42,10 +48,17 @@ const contracts = new Map<string, string>();
> should deposit and log transfers correctly
> should deposit and log fee token transfers correctly
> should transfer out locked assets for a valid transfer vm
> should deposit and log transfer with payload correctly
> should transfer out locked assets for a valid transfer with payload vm
> should mint bridged assets wrappers on transfer from another chain and handle fees correctly
> should handle additional data on token bridge transfer with payload in single transaction when feeRecipient == transferRecipient
> should not allow a redemption from msg.sender other than 'to' on token bridge transfer with payload
> should allow a redemption from msg.sender == 'to' on token bridge transfer with payload and check that sender recieves fee
> should burn bridged assets wrappers on transfer to another chain
> should handle ETH deposits correctly (uusd)
> should handle ETH withdrawals and fees correctly (uusd)
> should handle ETH deposits with payload correctly (uusd)
> should handle ETH withdrawals with payload correctly (uusd)
> should revert on transfer out of a total of > max(uint64) tokens
*/
@ -92,8 +105,18 @@ describe("Bridge Tests", () => {
wrapped_asset_code_id: wrappedAssetCodeId,
});
// mock bridge integration
const mockBridgeIntegration = await deploy(
client,
wallet,
WASM_MOCK_BRIDGE_INTEGRATION,
{
token_bridge_contract: tokenBridge,
}
);
contracts.set("wormhole", wormhole);
contracts.set("tokenBridge", tokenBridge);
contracts.set("mockBridgeIntegration", mockBridgeIntegration);
done();
} catch (e) {
console.error(e);
@ -200,14 +223,11 @@ describe("Bridge Tests", () => {
);
// check balances
let balanceBefore = new Int(0);
{
const [balance] = await client.bank.balance(tokenBridge);
const coin = balance.get(denom);
if (coin !== undefined) {
balanceBefore = new Int(coin.amount);
}
}
const balanceBefore = await getNativeBalance(
client,
tokenBridge,
denom
);
// execute outbound transfer
const receipt = await transactWithoutMemo(client, wallet, [
@ -216,17 +236,8 @@ describe("Bridge Tests", () => {
]);
console.info("receipt", receipt.txhash);
let balanceAfter: Int;
{
const [balance] = await client.bank.balance(tokenBridge);
const coin = balance.get(denom);
expect(!coin).toBeFalsy();
balanceAfter = new Int(coin!.amount);
}
expect(
balanceBefore.add(new Int(amount)).eq(balanceAfter)
).toBeTruthy();
const balanceAfter = await getNativeBalance(client, tokenBridge, denom);
expect(balanceBefore.add(amount).eq(balanceAfter)).toBeTruthy();
done();
} catch (e) {
@ -247,18 +258,7 @@ describe("Bridge Tests", () => {
const relayerFee = "1000000"; // one dolla
const walletAddress = wallet.key.accAddress;
const recipient = "terra17lmam6zguazs5q5u6z5mmx76uj63gldnse2pdp";
// check balances
let balanceBefore = new Int(0);
{
const [balance] = await client.bank.balance(recipient);
const coin = balance.get(denom);
if (coin !== undefined) {
balanceBefore = new Int(coin.amount);
}
}
const recipient = "terra17lmam6zguazs5q5u6z5mmx76uj63gldnse2pdp"; // test2
const encodedTo = nativeToHex(recipient);
console.log("encodedTo", encodedTo);
const ustAddress =
@ -292,28 +292,74 @@ describe("Bridge Tests", () => {
);
console.info("signedVaa", signedVaa);
// check balances
const walletBalanceBefore = await getNativeBalance(
client,
walletAddress,
denom
);
const recipientBalanceBefore = await getNativeBalance(
client,
recipient,
denom
);
const bridgeBalanceBefore = await getNativeBalance(
client,
tokenBridge,
denom
);
const submitVaa = new MsgExecuteContract(walletAddress, tokenBridge, {
submit_vaa: {
data: Buffer.from(signedVaa, "hex").toString("base64"),
},
});
// execute inbound transfer with signed vaa
// execute outbound transfer with signed vaa
const receipt = await transactWithoutMemo(client, wallet, [submitVaa]);
console.info("receipt", receipt.txhash);
let balanceAfter: Int;
{
const [balance] = await client.bank.balance(recipient);
const coin = balance.get(denom);
expect(!coin).toBeFalsy();
// check wallet (relayer) balance change
const walletBalanceAfter = await getNativeBalance(
client,
walletAddress,
denom
);
const gasPaid = computeGasPaid(receipt);
const walletExpectedChange = new Int(relayerFee).sub(gasPaid);
balanceAfter = new Int(coin!.amount);
}
const expectedAmount = (new Int(amount)).sub(relayerFee);
// due to rounding, we should expect the balances to reconcile
// within 1 unit (equivalent to 1e-6 uusd). Best-case scenario
// we end up with slightly more balance than expected
const reconciled = walletBalanceAfter
.minus(walletExpectedChange)
.minus(walletBalanceBefore);
expect(
//balanceBefore.add(new Int(expectedAmount)).eq(balanceAfter)
balanceBefore.add(expectedAmount).eq(balanceAfter)
reconciled.greaterThanOrEqualTo("0") &&
reconciled.lessThanOrEqualTo("1")
).toBeTruthy();
const recipientBalanceAfter = await getNativeBalance(
client,
recipient,
denom
);
const recipientExpectedChange = new Int(amount).sub(relayerFee);
expect(
recipientBalanceBefore
.add(recipientExpectedChange)
.eq(recipientBalanceAfter)
).toBeTruthy();
// cehck bridge balance change
const bridgeExpectedChange = new Int(amount);
const bridgeBalanceAfter = await getNativeBalance(
client,
tokenBridge,
denom
);
expect(
bridgeBalanceBefore.sub(bridgeExpectedChange).eq(bridgeBalanceAfter)
).toBeTruthy();
done();
@ -323,6 +369,305 @@ describe("Bridge Tests", () => {
}
})();
});
// transfer with payload tests
test("Initiate Transfer With Payload (native denom)", (done) => {
(async () => {
try {
const [client, wallet] = await makeProviderAndWallet();
const tokenBridge = contracts.get("tokenBridge")!;
// transfer uusd
const denom = "uusd";
const recipientAddress =
"0000000000000000000000004206942069420694206942069420694206942069";
const amount = "100000000"; // one benjamin
const relayerFee = "1000000"; // one dolla
const myPayload = "ABC";
const walletAddress = wallet.key.accAddress;
// need to deposit before initiating transfer
const deposit = new MsgExecuteContract(
wallet.key.accAddress,
tokenBridge,
{
deposit_tokens: {},
},
{ [denom]: amount }
);
const initiateTransferWithPayload = new MsgExecuteContract(
walletAddress,
tokenBridge as string,
{
initiate_transfer_with_payload: {
asset: {
amount,
info: {
native_token: {
denom,
},
},
},
recipient_chain: 2,
recipient: Buffer.from(recipientAddress, "hex").toString(
"base64"
),
fee: relayerFee,
payload: Buffer.from(myPayload, "hex").toString("base64"),
nonce: 69,
},
}
);
// check balances
const balanceBefore = await getNativeBalance(
client,
tokenBridge,
denom
);
// execute outbound transfer with payload
const receipt = await transactWithoutMemo(client, wallet, [
deposit,
initiateTransferWithPayload,
]);
console.info("receipt txHash", receipt.txhash);
const balanceAfter = await getNativeBalance(client, tokenBridge, denom);
expect(balanceBefore.add(amount).eq(balanceAfter)).toBeTruthy();
done();
} catch (e) {
console.error(e);
done("Failed to Initiate Transfer With Payload (native denom)");
}
})();
});
test("Complete Transfer With Payload (native denom)", (done) => {
(async () => {
try {
const [client, wallet] = await makeProviderAndWallet();
const tokenBridge = contracts.get("tokenBridge")!;
const mockBridgeIntegration = contracts.get("mockBridgeIntegration")!;
const denom = "uusd";
const amount = "100000000"; // one benjamin
const relayerFee = "1000000"; // one dolla
const walletAddress = wallet.key.accAddress;
const encodedTo = nativeToHex(mockBridgeIntegration);
console.log("encodedTo", encodedTo);
const ustAddress =
"0100000000000000000000000000000000000000000000000000000075757364";
const additionalPayload = "All your base are belong to us";
const vaaPayload = makeTransferVaaPayload(
3,
amount,
ustAddress,
encodedTo,
3,
relayerFee,
additionalPayload
);
console.info("vaaPayload", vaaPayload);
const timestamp = 1;
const nonce = 1;
const sequence = 2;
const signedVaa = signAndEncodeVaa(
timestamp,
nonce,
FOREIGN_CHAIN,
FOREIGN_TOKEN_BRIDGE,
sequence,
vaaPayload,
TEST_SIGNER_PKS,
GUARDIAN_SET_INDEX,
CONSISTENCY_LEVEL
);
console.info("signedVaa", signedVaa);
// check balances before execute
const walletBalanceBefore = await getNativeBalance(
client,
walletAddress,
denom
);
const contractBalanceBefore = await getNativeBalance(
client,
mockBridgeIntegration,
denom
);
const bridgeBalanceBefore = await getNativeBalance(
client,
tokenBridge,
denom
);
const submitVaa = new MsgExecuteContract(
walletAddress,
mockBridgeIntegration,
{
complete_transfer_with_payload: {
data: Buffer.from(signedVaa, "hex").toString("base64"),
},
}
);
// execute outbound transfer with signed vaa
const receipt = await transactWithoutMemo(client, wallet, [submitVaa]);
console.info("receipt txHash", receipt.txhash);
// check wallet (relayer) balance change
const walletBalanceAfter = await getNativeBalance(
client,
walletAddress,
denom
);
const gasPaid = computeGasPaid(receipt);
const walletExpectedChange = new Int(relayerFee).sub(gasPaid);
// due to rounding, we should expect the balances to reconcile
// within 1 unit (equivalent to 1e-6 uusd). Best-case scenario
// we end up with slightly more balance than expected
const reconciled = walletBalanceAfter
.minus(walletExpectedChange)
.minus(walletBalanceBefore);
expect(
reconciled.greaterThanOrEqualTo("0") &&
reconciled.lessThanOrEqualTo("1")
).toBeTruthy();
// check contract balance change
const contractBalanceAfter = await getNativeBalance(
client,
mockBridgeIntegration,
denom
);
const contractExpectedChange = new Int(amount).sub(relayerFee);
expect(
contractBalanceBefore
.add(contractExpectedChange)
.eq(contractBalanceAfter)
).toBeTruthy();
// cehck bridge balance change
const bridgeExpectedChange = new Int(amount);
const bridgeBalanceAfter = await getNativeBalance(
client,
tokenBridge,
denom
);
expect(
bridgeBalanceBefore.sub(bridgeExpectedChange).eq(bridgeBalanceAfter)
).toBeTruthy();
// verify payload
const events = parseEventsFromLog(receipt);
const response: any[] = events.find((event) => {
return event.type == "wasm";
}).attributes;
const transferPayloadResponse = response.find((item) => {
return item.key == "transfer_payload";
});
expect(
Buffer.from(transferPayloadResponse.value, "base64").toString()
).toEqual(additionalPayload);
done();
} catch (e) {
console.error(e);
done("Failed to Complete Transfer With Payload (native denom)");
}
})();
});
test("Throw on Complete Transfer With Payload If Someone Else Redeems VAA", (done) => {
(async () => {
try {
const [client, wallet] = await makeProviderAndWallet();
const tokenBridge = contracts.get("tokenBridge")!;
const mockBridgeIntegration = contracts.get("mockBridgeIntegration")!;
const denom = "uusd";
const amount = "100000000"; // one benjamin
const relayerFee = "1000000"; // one dolla
const walletAddress = wallet.key.accAddress;
const encodedTo = nativeToHex(mockBridgeIntegration);
console.log("encodedTo", encodedTo);
const ustAddress =
"0100000000000000000000000000000000000000000000000000000075757364";
const additionalPayload = "All your base are belong to us";
const vaaPayload = makeTransferVaaPayload(
3,
amount,
ustAddress,
encodedTo,
3,
relayerFee,
additionalPayload
);
console.info("vaaPayload", vaaPayload);
const timestamp = 1;
const nonce = 1;
const sequence = 3;
const signedVaa = signAndEncodeVaa(
timestamp,
nonce,
FOREIGN_CHAIN,
FOREIGN_TOKEN_BRIDGE,
sequence,
vaaPayload,
TEST_SIGNER_PKS,
GUARDIAN_SET_INDEX,
CONSISTENCY_LEVEL
);
console.info("signedVaa", signedVaa);
let expectedErrorFound = false;
try {
const submitVaa = new MsgExecuteContract(walletAddress, tokenBridge, {
complete_transfer_with_payload: {
data: Buffer.from(signedVaa, "hex").toString("base64"),
relayer: walletAddress,
},
});
// execute outbound transfer with signed vaa
const receipt = await transactWithoutMemo(client, wallet, [
submitVaa,
]);
console.info("receipt txHash", receipt.txhash);
} catch (e) {
const errorMsg: string = e.response.data.message;
expectedErrorFound = errorMsg.includes(
"transfers with payload can only be redeemed by the recipient"
);
}
expect(expectedErrorFound).toBeTruthy();
done();
} catch (e) {
console.error(e);
done(
"Failed to Throw on Complete Transfer With Payload If Someone Else Redeems VAA"
);
}
})();
});
});
function nativeToHex(address: string) {

View File

@ -1,16 +1,23 @@
import {
BlockTxBroadcastResult,
Int,
LCDClient,
MnemonicKey,
Msg,
Wallet,
} from "@terra-money/terra.js";
export const GAS_PRICE = 0.2; // uusd
export async function makeProviderAndWallet(): Promise<[LCDClient, Wallet]> {
// provider
const client = new LCDClient({
URL: "http://localhost:1317",
chainID: "localterra",
gasAdjustment: "2",
gasPrices: {
uusd: GAS_PRICE,
},
});
// wallet
@ -48,3 +55,16 @@ export async function transactWithoutMemo(
): Promise<BlockTxBroadcastResult> {
return transact(client, wallet, msgs, "");
}
export async function getNativeBalance(
client: LCDClient,
address: string,
denom: string
): Promise<Int> {
const [balance] = await client.bank.balance(address);
const coin = balance.get(denom);
if (coin === undefined) {
return new Int(0);
}
return new Int(coin.amount);
}

View File

@ -0,0 +1,14 @@
import { BlockTxBroadcastResult, Coin, Int } from "@terra-money/terra.js";
import { GAS_PRICE } from "./client";
export function parseEventsFromLog(receipt: BlockTxBroadcastResult): any[] {
return JSON.parse(receipt.raw_log)[0].events;
}
export function computeGasPaid(receipt: BlockTxBroadcastResult): Int {
const gasPrice = new Coin("uusd", GAS_PRICE).amount;
// LocalTerra seems to spend all the gas_wanted
// instead of spending gas_used...
return new Int(gasPrice.mul(receipt.gas_wanted).ceil());
}

View File

@ -17,12 +17,13 @@ import { zeroPad } from "ethers/lib/utils.js";
*/
const artifacts = [
"wormhole.wasm",
"token_bridge.wasm",
"token_bridge_terra.wasm",
"cw20_wrapped.wasm",
"cw20_base.wasm",
"nft_bridge.wasm",
"cw721_wrapped.wasm",
"cw721_base.wasm",
"mock_bridge_integration.wasm",
];
/* Check that the artifact folder contains all the wasm files we expect and nothing else */
@ -166,7 +167,7 @@ addresses["wormhole.wasm"] = await instantiate("wormhole.wasm", {
},
});
addresses["token_bridge.wasm"] = await instantiate("token_bridge.wasm", {
addresses["token_bridge_terra.wasm"] = await instantiate("token_bridge_terra.wasm", {
gov_chain: govChain,
gov_address: Buffer.from(govAddress, "hex").toString("base64"),
wormhole_contract: addresses["wormhole.wasm"],
@ -239,7 +240,7 @@ await mint_cw721(
/* Registrations: tell the bridge contracts to know about each other */
const contract_registrations = {
"token_bridge.wasm": [
"token_bridge_terra.wasm": [
// Solana
process.env.REGISTER_SOL_TOKEN_BRIDGE_VAA,
// Ethereum

View File

@ -15,7 +15,7 @@ export const TERRA_GAS_PRICES_URL = "https://fcd.terra.dev/v1/txs/gas_prices";
const argv = yargs(hideBin(process.argv))
.option('network', {
description: 'Which network to deploy to',
choices: ['mainnet', 'testnet', 'localterra'],
choices: ['mainnet', 'testnet', 'devnet'],
required: true
})
.option('artifact', {
@ -50,8 +50,7 @@ const terra_host =
}
: {
URL: "http://localhost:1317",
chainID: "columbus-5",
name: "localterra",
chainID: "localterra",
};
const lcd = new LCDClient(terra_host);