cosmwasm: Add wormchain-accounting contract (#1920)

* sdk/rust: Move profile settings to workspace

* sdk/rust: Add serde_wormhole crate

The serde_wormhole crate implements the wormhole wire format as a serde
data format.  This will let us replace all the hand-rolled
serialization with auto-generated code, which is less error-prone and
easier to review.

* sdk/rust: Add serde-based struct defintions

Refactor the core crate to add serde-based struct definitions for the
various messages used by the different wormhole smart contracts.  This
will also make it easier to use alternate data formats (like json) for
client-side tooling.

Co-authored-by: Reisen <reisen@morphism.org>

* sdk/rust: Drop references to `de::Unexpected`

The `de::Unexpected` enum from serde has a `Float(f64)` variant.
Referencing this enum anywhere in the code will cause the compiler to
emit its `fmt::Display` impl, which includes an `f64.load` instruction
on wasm targets.  Even if this instruction is never executed, its mere
existence will cause cosmos chains to reject any cosmwasm contract that
has it.

Fix this by removing all references to `de::Unexpected`.

* cosmwasm: Use cargo resolver version "2"

Enable the new feature resolver for the entire workspace.  This
prevents features that are enabled only for dev builds from also being
enabled in normal builds.

* Move cosmwasm Dockerfile to root directory

The cosmwasm contracts now also depend on the rust sdk so the docker
build context needs to be set to the root directory rather than the
cosmwasm/ directory.

* cosmwasm: Add wormchain-accounting contract

This contract implements tokenbridge accounting specifically for the
wormchain environment.

Fixes #1880.

* cosmwasm/accounting: Drop references to `de::Unexpected`

The `de::Unexpected` enum from serde has a `Float(f64)` variant.
Referencing this enum anywhere in the code will cause the compiler to
emit its `fmt::Display` impl, which includes an `f64.load` instruction
on wasm targets.  Even if this instruction is never executed, its mere
existence will cause cosmos chains to reject any cosmwasm contracts that
contain it.

Fix this by removing references to `de::Unexpected`.

Co-authored-by: Reisen <reisen@morphism.org>
This commit is contained in:
Chirantan Ekbote 2022-12-15 02:06:45 +09:00 committed by GitHub
parent 39f5877756
commit 25abafc753
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 4234 additions and 302 deletions

View File

@ -3,10 +3,12 @@
# 2. The second is an empty image with only the wasm files (useful for exporting)
# 3. The third creates a node.js environment to deploy the contracts to devnet
FROM cosmwasm/workspace-optimizer:0.12.6@sha256:e6565a5e87c830ef3e8775a9035006b38ad0aaf0a96319158c802457b1dd1d08 AS builder
COPY Cargo.lock /code/
COPY Cargo.toml /code/
COPY contracts /code/contracts
COPY packages /code/packages
COPY cosmwasm/Cargo.lock /code/
COPY cosmwasm/Cargo.toml /code/
COPY cosmwasm/contracts /code/contracts
COPY cosmwasm/packages /code/packages
COPY sdk/rust /sdk/rust
# Support additional root CAs
COPY README.md cert.pem* /certs/
@ -34,9 +36,9 @@ RUN apt update && apt install netcat curl jq -y
WORKDIR /app/tools
COPY --from=artifacts / /app/artifacts
COPY ./artifacts/cw20_base.wasm /app/artifacts/
COPY ./cosmwasm/artifacts/cw20_base.wasm /app/artifacts/
COPY ./tools/package.json ./tools/package-lock.json /app/tools/
COPY ./cosmwasm/tools/package.json ./cosmwasm/tools/package-lock.json /app/tools/
RUN --mount=type=cache,uid=1000,gid=1000,target=/home/node/.npm \
npm ci
COPY ./tools /app/tools
COPY ./cosmwasm/tools /app/tools

View File

@ -599,8 +599,8 @@ if terra2:
docker_build(
ref = "terra2-contracts",
context = "./cosmwasm",
dockerfile = "./cosmwasm/Dockerfile",
context = ".",
dockerfile = "./Dockerfile.cosmwasm",
)
k8s_yaml_with_ns("devnet/terra2-devnet.yaml")

747
cosmwasm/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -7,9 +7,14 @@ members = [
"contracts/shutdown-token-bridge",
"contracts/mock-bridge-integration",
"packages/accounting",
"contracts/wormchain-accounting",
"packages/wormhole-bindings",
]
# Needed to prevent unwanted feature unification between normal builds and dev builds. See
# https://doc.rust-lang.org/cargo/reference/resolver.html#resolver-versions for more details.
resolver = "2"
[profile.release]
opt-level = 3
debug = false
@ -24,6 +29,9 @@ overflow-checks = true
[patch.crates-io]
accounting = { path = "packages/accounting" }
cw20-wrapped-2 = { path = "contracts/cw20-wrapped" }
serde_wormhole = { path = "../sdk/rust/serde_wormhole" }
token-bridge-terra-2 = { path = "contracts/token-bridge" }
wormchain-accounting = { path = "contracts/wormchain-accounting" }
wormhole-bindings = { path = "packages/wormhole-bindings" }
wormhole-bridge-terra-2 = { path = "contracts/wormhole" }
wormhole-core = { path = "../sdk/rust/core" }

View File

@ -22,7 +22,7 @@ ifndef VALID_$(NETWORK)
endif
$(WASMS) artifacts/checksums.txt: $(SOURCE_FILES)
DOCKER_BUILDKIT=1 docker build --target artifacts -o artifacts .
DOCKER_BUILDKIT=1 docker build --target artifacts -o artifacts -f ../Dockerfile.cosmwasm ../
payer-$(NETWORK).json:
$(error Missing private key in payer-$(NETWORK).json)

View File

@ -0,0 +1,37 @@
[package]
name = "wormchain-accounting"
version = "0.1.0"
authors = ["Wormhole Project Contributors"]
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
backtraces = ["cosmwasm-std/backtraces"]
# use library feature to disable all instantiate/execute/query exports
library = []
[dependencies]
accounting = "0.1.0"
anyhow = "1"
base64 = "0.13"
cosmwasm-schema = "1"
cosmwasm-std = "1"
cosmwasm-storage = "1"
cw-storage-plus = "0.13.2"
cw2 = "0.13.2"
hex = "0.4.3"
schemars = "0.8.8"
serde = { version = "1.0.137", default-features = false, features = ["derive"] }
serde_wormhole = "0.1.0"
thiserror = { version = "1.0.31" }
tinyvec = { version = "1.6", default-features = false, features = ["alloc", "serde"]}
tokenbridge = { package = "token-bridge-terra-2", version = "0.1.0", features = ["library"] }
wormhole-bindings = "0.1.0"
wormhole-core = "0.1.0"
[dev-dependencies]
anyhow = { version = "1", features = ["backtrace"] }
cw-multi-test = "0.13.2"
wormhole-bindings = { version = "0.1", features = ["fake"] }

View File

@ -0,0 +1,11 @@
use cosmwasm_schema::write_api;
use wormchain_accounting::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
fn main() {
write_api! {
instantiate: InstantiateMsg,
execute: ExecuteMsg,
query: QueryMsg,
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,461 @@
use std::marker::PhantomData;
use accounting::{
query_balance, query_modification, query_transfer,
state::{account, transfer, Modification, TokenAddress, Transfer},
};
use anyhow::{ensure, Context};
#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{
from_binary, to_binary, Binary, ConversionOverflowError, CosmosMsg, Deps, DepsMut, Empty, Env,
Event, MessageInfo, Order, Response, StdError, StdResult, Uint256, WasmMsg,
};
use cw2::set_contract_version;
use cw_storage_plus::Bound;
use tinyvec::{Array, TinyVec};
use wormhole::token::Message;
use wormhole_bindings::{Signature, WormholeQuery};
use crate::{
bail,
error::{AnyError, ContractError},
msg::{
AllAccountsResponse, AllModificationsResponse, AllPendingTransfersResponse,
AllTransfersResponse, ExecuteMsg, Instantiate, InstantiateMsg, MigrateMsg, Observation,
QueryMsg, Upgrade,
},
state::{self, Data, PendingTransfer, PENDING_TRANSFERS, TOKENBRIDGE_ADDR},
};
// version info for migration info
const CONTRACT_NAME: &str = "crates.io:wormchain-accounting";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut<WormholeQuery>,
_env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, AnyError> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)
.context("failed to set contract version")?;
let _: Empty = deps
.querier
.query(
&WormholeQuery::VerifyQuorum {
data: msg.instantiate.clone(),
guardian_set_index: msg.guardian_set_index,
signatures: msg.signatures,
}
.into(),
)
.context(ContractError::VerifyQuorum)?;
let init: Instantiate =
from_binary(&msg.instantiate).context("failed to parse `Instantiate` message")?;
let tokenbridge_addr = deps
.api
.addr_validate(&init.tokenbridge_addr)
.context("failed to validate tokenbridge address")?;
TOKENBRIDGE_ADDR
.save(deps.storage, &tokenbridge_addr)
.context("failed to save tokenbridge address")?;
let event =
accounting::instantiate(deps, init.into()).context("failed to instantiate accounting")?;
Ok(Response::new()
.add_attribute("action", "instantiate")
.add_attribute("owner", info.sender)
.add_event(event))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(_deps: DepsMut<WormholeQuery>, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
Ok(Response::default())
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut<WormholeQuery>,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, AnyError> {
match msg {
ExecuteMsg::SubmitObservations {
observations,
guardian_set_index,
signature,
} => submit_observations(deps, info, observations, guardian_set_index, signature),
ExecuteMsg::ModifyBalance {
modification,
guardian_set_index,
signatures,
} => modify_balance(deps, info, modification, guardian_set_index, signatures),
ExecuteMsg::UpgradeContract {
upgrade,
guardian_set_index,
signatures,
} => upgrade_contract(deps, env, info, upgrade, guardian_set_index, signatures),
}
}
fn submit_observations(
mut deps: DepsMut<WormholeQuery>,
info: MessageInfo,
observations: Binary,
guardian_set_index: u32,
signature: Signature,
) -> Result<Response, AnyError> {
deps.querier
.query::<Empty>(
&WormholeQuery::VerifySignature {
data: observations.clone(),
guardian_set_index,
signature: signature.clone(),
}
.into(),
)
.context("failed to verify signature")?;
let quorum = deps
.querier
.query::<u32>(&WormholeQuery::CalculateQuorum { guardian_set_index }.into())
.and_then(|q| {
usize::try_from(q).map_err(|_| StdError::ConversionOverflow {
source: ConversionOverflowError::new("u32", "usize", q.to_string()),
})
})
.context("failed to calculate quorum")?;
let observations: Vec<Observation> =
from_binary(&observations).context("failed to parse `Observations`")?;
let events = observations
.into_iter()
.map(|o| {
handle_observation(
deps.branch(),
o,
guardian_set_index,
quorum,
signature.clone(),
)
})
.filter_map(Result::transpose)
.collect::<anyhow::Result<Vec<_>>>()
.context("failed to handle `Observation`")?;
Ok(Response::new()
.add_attribute("action", "submit_observations")
.add_attribute("owner", info.sender)
.add_events(events))
}
fn handle_observation(
mut deps: DepsMut<WormholeQuery>,
o: Observation,
guardian_set_index: u32,
quorum: usize,
sig: Signature,
) -> anyhow::Result<Option<Event>> {
if accounting::has_transfer(deps.as_ref(), o.key.clone()) {
bail!("transfer for key \"{}\" already committed", o.key);
}
let key = PENDING_TRANSFERS.key(o.key.clone());
let mut pending = key
.may_load(deps.storage)
.map(Option::unwrap_or_default)
.context("failed to load `PendingTransfer`")?;
let data = match pending
.iter_mut()
.find(|d| d.guardian_set_index() == guardian_set_index && d.observation() == &o)
{
Some(d) => d,
None => {
pending.push(Data::new(o.clone(), guardian_set_index));
let back = pending.len() - 1;
&mut pending[back]
}
};
data.add_signature(sig)?;
if data.signatures().len() < quorum {
// Still need more signatures so just save the pending transfer data and exit.
key.save(deps.storage, &pending)
.context("failed to save pending transfers")?;
return Ok(None);
}
let (msg, _) = serde_wormhole::from_slice_with_payload(&o.payload)
.context("failed to parse observation payload")?;
let tx_data = match msg {
Message::Transfer {
amount,
token_address,
token_chain,
recipient_chain,
..
}
| Message::TransferWithPayload {
amount,
token_address,
token_chain,
recipient_chain,
..
} => transfer::Data {
amount: Uint256::from_be_bytes(amount.0),
token_address: TokenAddress::new(token_address.0),
token_chain: token_chain.into(),
recipient_chain: recipient_chain.into(),
},
_ => bail!("Unknown tokenbridge payload"),
};
let emitter_chain = o.key.emitter_chain();
let tokenbridge_addr = TOKENBRIDGE_ADDR
.load(deps.storage)
.context("failed to load tokenbridge addr")?;
let registered_emitter: Vec<u8> = deps
.querier
.query_wasm_smart(
tokenbridge_addr,
&tokenbridge::msg::QueryMsg::ChainRegistration {
chain: emitter_chain,
},
)
.context("failed to query chain registration")?;
ensure!(
*registered_emitter == **o.key.emitter_address(),
"unknown emitter address"
);
accounting::commit_transfer(
deps.branch(),
Transfer {
key: o.key.clone(),
data: tx_data,
},
)
.context("failed to commit transfer")?;
// Now that the transfer has been committed, we don't need to keep it in the pending list.
key.remove(deps.storage);
Ok(Some(
Event::new("Transfer")
.add_attribute("emitter_chain", o.key.emitter_chain().to_string())
.add_attribute("emitter_address", o.key.emitter_address().to_string())
.add_attribute("sequence", o.key.sequence().to_string())
.add_attribute("nonce", o.nonce.to_string())
.add_attribute("tx_hash", o.tx_hash.to_base64())
.add_attribute("payload", o.payload.to_base64()),
))
}
fn modify_balance(
deps: DepsMut<WormholeQuery>,
info: MessageInfo,
modification: Binary,
guardian_set_index: u32,
signatures: Vec<Signature>,
) -> Result<Response, AnyError> {
deps.querier
.query::<Empty>(
&WormholeQuery::VerifyQuorum {
data: modification.clone(),
guardian_set_index,
signatures: signatures.into_iter().map(From::from).collect(),
}
.into(),
)
.context(ContractError::VerifyQuorum)?;
let msg: Modification = from_binary(&modification).context("failed to parse `Modification`")?;
let event =
accounting::modify_balance(deps, msg).context("failed to modify account balance")?;
Ok(Response::new()
.add_attribute("action", "modify_balance")
.add_attribute("owner", info.sender)
.add_event(event))
}
fn upgrade_contract(
deps: DepsMut<WormholeQuery>,
env: Env,
info: MessageInfo,
upgrade: Binary,
guardian_set_index: u32,
signatures: Vec<Signature>,
) -> Result<Response, AnyError> {
deps.querier
.query::<Empty>(
&WormholeQuery::VerifyQuorum {
data: upgrade.clone(),
guardian_set_index,
signatures: signatures.into_iter().map(From::from).collect(),
}
.into(),
)
.context(ContractError::VerifyQuorum)?;
let Upgrade { new_addr } = from_binary(&upgrade).context("failed to parse `Upgrade`")?;
let mut buf = 0u64.to_ne_bytes();
buf.copy_from_slice(&new_addr[24..]);
let new_contract = u64::from_be_bytes(buf);
Ok(Response::new()
.add_message(CosmosMsg::Wasm(WasmMsg::Migrate {
contract_addr: env.contract.address.to_string(),
new_code_id: new_contract,
msg: to_binary(&MigrateMsg {})?,
}))
.add_attribute("action", "contract_upgrade")
.add_attribute("owner", info.sender))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps<WormholeQuery>, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::Balance(key) => query_balance(deps, key).and_then(|resp| to_binary(&resp)),
QueryMsg::AllAccounts { start_after, limit } => {
query_all_accounts(deps, start_after, limit).and_then(|resp| to_binary(&resp))
}
QueryMsg::Transfer(req) => query_transfer(deps, req).and_then(|resp| to_binary(&resp)),
QueryMsg::AllTransfers { start_after, limit } => {
query_all_transfers(deps, start_after, limit).and_then(|resp| to_binary(&resp))
}
QueryMsg::PendingTransfer(req) => {
query_pending_transfer(deps, req).and_then(|resp| to_binary(&resp))
}
QueryMsg::AllPendingTransfers { start_after, limit } => {
query_all_pending_transfers(deps, start_after, limit).and_then(|resp| to_binary(&resp))
}
QueryMsg::Modification { sequence } => {
query_modification(deps, sequence).and_then(|resp| to_binary(&resp))
}
QueryMsg::AllModifications { start_after, limit } => {
query_all_modifications(deps, start_after, limit).and_then(|resp| to_binary(&resp))
}
}
}
fn query_all_accounts(
deps: Deps<WormholeQuery>,
start_after: Option<account::Key>,
limit: Option<u32>,
) -> StdResult<AllAccountsResponse> {
if let Some(lim) = limit {
let l = lim
.try_into()
.map_err(|_| ConversionOverflowError::new("u32", "usize", lim.to_string()))?;
accounting::query_all_accounts(deps, start_after)
.take(l)
.collect::<StdResult<Vec<_>>>()
.map(|accounts| AllAccountsResponse { accounts })
} else {
accounting::query_all_accounts(deps, start_after)
.collect::<StdResult<Vec<_>>>()
.map(|accounts| AllAccountsResponse { accounts })
}
}
fn query_all_transfers(
deps: Deps<WormholeQuery>,
start_after: Option<transfer::Key>,
limit: Option<u32>,
) -> StdResult<AllTransfersResponse> {
if let Some(lim) = limit {
let l = lim
.try_into()
.map_err(|_| ConversionOverflowError::new("u32", "usize", lim.to_string()))?;
accounting::query_all_transfers(deps, start_after)
.take(l)
.collect::<StdResult<Vec<_>>>()
.map(|transfers| AllTransfersResponse { transfers })
} else {
accounting::query_all_transfers(deps, start_after)
.collect::<StdResult<Vec<_>>>()
.map(|transfers| AllTransfersResponse { transfers })
}
}
#[inline]
fn tinyvec_to_vec<A: Array>(tv: TinyVec<A>) -> Vec<A::Item> {
match tv {
TinyVec::Inline(mut arr) => arr.drain_to_vec(),
TinyVec::Heap(v) => v,
}
}
fn query_pending_transfer(
deps: Deps<WormholeQuery>,
key: transfer::Key,
) -> StdResult<Vec<state::Data>> {
PENDING_TRANSFERS
.load(deps.storage, key)
.map(tinyvec_to_vec)
}
fn query_all_pending_transfers(
deps: Deps<WormholeQuery>,
start_after: Option<transfer::Key>,
limit: Option<u32>,
) -> StdResult<AllPendingTransfersResponse> {
let start = start_after.map(|key| Bound::Exclusive((key, PhantomData)));
let iter = PENDING_TRANSFERS
.range(deps.storage, start, None, Order::Ascending)
.map(|item| {
item.map(|(key, tv)| PendingTransfer {
key,
data: tinyvec_to_vec(tv),
})
});
if let Some(lim) = limit {
let l = lim
.try_into()
.map_err(|_| ConversionOverflowError::new("u32", "usize", lim.to_string()))?;
iter.take(l)
.collect::<StdResult<Vec<_>>>()
.map(|pending| AllPendingTransfersResponse { pending })
} else {
iter.collect::<StdResult<Vec<_>>>()
.map(|pending| AllPendingTransfersResponse { pending })
}
}
fn query_all_modifications(
deps: Deps<WormholeQuery>,
start_after: Option<u64>,
limit: Option<u32>,
) -> StdResult<AllModificationsResponse> {
if let Some(lim) = limit {
let l = lim
.try_into()
.map_err(|_| ConversionOverflowError::new("u32", "usize", lim.to_string()))?;
accounting::query_all_modifications(deps, start_after)
.take(l)
.collect::<StdResult<Vec<_>>>()
.map(|modifications| AllModificationsResponse { modifications })
} else {
accounting::query_all_modifications(deps, start_after)
.collect::<StdResult<Vec<_>>>()
.map(|modifications| AllModificationsResponse { modifications })
}
}

View File

@ -0,0 +1,59 @@
use std::ops::{Deref, DerefMut};
use anyhow::anyhow;
use cosmwasm_std::StdError;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ContractError {
#[error("failed to verify quorum")]
VerifyQuorum,
}
// This is a workaround for the fact that `cw_multi_test::ContractWrapper` doesn't support contract
// functions returning `anyhow::Error` directly.
#[derive(Error, Debug)]
#[repr(transparent)]
#[error("{0:#}")]
pub struct AnyError(#[from] anyhow::Error);
impl Deref for AnyError {
type Target = anyhow::Error;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for AnyError {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<StdError> for AnyError {
fn from(e: StdError) -> AnyError {
anyhow!(e).into()
}
}
impl From<ContractError> for AnyError {
fn from(e: ContractError) -> AnyError {
anyhow!(e).into()
}
}
// Workaround for not being able to use the `bail!` macro directly.
#[doc(hidden)]
#[macro_export]
macro_rules! bail {
($msg:literal $(,)?) => {
return ::core::result::Result::Err(::anyhow::anyhow!($msg).into())
};
($err:expr $(,)?) => {
return ::core::result::Result::Err(::anyhow::anyhow!($err).into())
};
($fmt:expr, $($arg:tt)*) => {
return ::core::result::Result::Err(::anyhow::anyhow!($fmt, $($arg)*).into())
};
}

View File

@ -0,0 +1,6 @@
pub mod contract;
mod error;
pub mod msg;
pub mod state;
pub use crate::error::ContractError;

View File

@ -0,0 +1,145 @@
use accounting::state::{account, transfer, Account, Modification, Transfer};
use cosmwasm_schema::{cw_serde, QueryResponses};
use cosmwasm_std::Binary;
use wormhole_bindings::Signature;
use crate::state::{self, PendingTransfer};
#[cw_serde]
pub struct Instantiate {
pub tokenbridge_addr: String,
pub accounts: Vec<Account>,
pub transfers: Vec<Transfer>,
pub modifications: Vec<Modification>,
}
impl From<Instantiate> for accounting::msg::Instantiate {
fn from(i: Instantiate) -> Self {
Self {
accounts: i.accounts,
transfers: i.transfers,
modifications: i.modifications,
}
}
}
#[cw_serde]
pub struct InstantiateMsg {
// A serialized `Instantiate` message.
pub instantiate: Binary,
// The index of the guardian set used to sign this message.
pub guardian_set_index: u32,
// A quorum of signatures for `instantiate`.
pub signatures: Vec<Signature>,
}
#[cw_serde]
#[derive(Default)]
pub struct Observation {
// The key that uniquely identifies the observation.
pub key: transfer::Key,
// The nonce for the transfer.
pub nonce: u32,
// The hash of the transaction on the emitter chain in which the transfer
// was performed.
pub tx_hash: Binary,
// The serialized tokenbridge payload.
pub payload: Binary,
}
#[cw_serde]
pub struct Upgrade {
pub new_addr: [u8; 32],
}
#[cw_serde]
pub enum ExecuteMsg {
SubmitObservations {
// A serialized `Vec<Observation>`. Multiple observations can be submitted together to reduce
// transaction overhead.
observations: Binary,
// The index of the guardian set used to sign the observations.
guardian_set_index: u32,
// A signature for `observations`.
signature: Signature,
},
ModifyBalance {
// A serialized `Modification` message.
modification: Binary,
// The index of the guardian set used to sign this modification.
guardian_set_index: u32,
// A quorum of signatures for `modification`.
signatures: Vec<Signature>,
},
UpgradeContract {
// A serialized `Upgrade` message.
upgrade: Binary,
// The index of the guardian set used to sign this request.
guardian_set_index: u32,
// A quorum of signatures for `key`.
signatures: Vec<Signature>,
},
}
#[cw_serde]
pub struct MigrateMsg {}
#[cw_serde]
#[derive(QueryResponses)]
pub enum QueryMsg {
#[returns(account::Balance)]
Balance(account::Key),
#[returns(AllAccountsResponse)]
AllAccounts {
start_after: Option<account::Key>,
limit: Option<u32>,
},
#[returns(transfer::Data)]
Transfer(transfer::Key),
#[returns(AllTransfersResponse)]
AllTransfers {
start_after: Option<transfer::Key>,
limit: Option<u32>,
},
#[returns(state::Data)]
PendingTransfer(transfer::Key),
#[returns(AllPendingTransfersResponse)]
AllPendingTransfers {
start_after: Option<transfer::Key>,
limit: Option<u32>,
},
#[returns(Modification)]
Modification { sequence: u64 },
#[returns(AllModificationsResponse)]
AllModifications {
start_after: Option<u64>,
limit: Option<u32>,
},
}
#[cw_serde]
pub struct AllAccountsResponse {
pub accounts: Vec<Account>,
}
#[cw_serde]
pub struct AllTransfersResponse {
pub transfers: Vec<Transfer>,
}
#[cw_serde]
pub struct AllPendingTransfersResponse {
pub pending: Vec<PendingTransfer>,
}
#[cw_serde]
pub struct AllModificationsResponse {
pub modifications: Vec<Modification>,
}

View File

@ -0,0 +1,69 @@
use accounting::state::transfer;
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Addr;
use cw_storage_plus::{Item, Map};
use thiserror::Error;
use tinyvec::TinyVec;
use wormhole_bindings::Signature;
use crate::msg::Observation;
pub const TOKENBRIDGE_ADDR: Item<Addr> = Item::new("tokenbride_addr");
pub const PENDING_TRANSFERS: Map<transfer::Key, TinyVec<[Data; 2]>> = Map::new("pending_transfers");
#[cw_serde]
pub struct PendingTransfer {
pub key: transfer::Key,
pub data: Vec<Data>,
}
#[derive(Error, Debug)]
#[error("cannot submit duplicate signatures for the same observation")]
pub struct DuplicateSignatureError;
#[cw_serde]
#[derive(Default)]
pub struct Data {
observation: Observation,
guardian_set_index: u32,
signatures: Vec<Signature>,
}
impl Data {
pub const fn new(observation: Observation, guardian_set_index: u32) -> Self {
Self {
observation,
guardian_set_index,
signatures: Vec::new(),
}
}
pub fn observation(&self) -> &Observation {
&self.observation
}
pub fn guardian_set_index(&self) -> u32 {
self.guardian_set_index
}
pub fn signatures(&self) -> &[Signature] {
&self.signatures
}
/// Adds `sig` to the list of signatures for this transfer data. Returns true if `sig`
/// was successfully added or false if `sig` was already in the signature list.
pub fn add_signature(&mut self, sig: Signature) -> Result<(), DuplicateSignatureError> {
match self
.signatures
.binary_search_by_key(&sig.index, |s| s.index)
{
Ok(_) => Err(DuplicateSignatureError),
Err(idx) => {
self.signatures.insert(idx, sig);
Ok(())
}
}
}
}

View File

@ -0,0 +1,23 @@
use cosmwasm_std::{
to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdError, StdResult,
};
use tokenbridge::msg::QueryMsg;
pub fn instantiate(_: DepsMut, _: Env, _: MessageInfo, _: Empty) -> StdResult<Response> {
Ok(Response::new())
}
pub fn execute(_: DepsMut, _: Env, _: MessageInfo, _: Empty) -> StdResult<Response> {
Err(StdError::GenericErr {
msg: "execute not implemented".into(),
})
}
pub fn query(_: Deps, _: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::ChainRegistration { chain } => to_binary(&vec![chain as u8; 32]),
_ => Err(StdError::GenericErr {
msg: "unimplemented query message".into(),
}),
}
}

View File

@ -0,0 +1,310 @@
#![allow(dead_code)]
use accounting::state::{account, transfer, Account, Kind, Modification, Transfer};
use cosmwasm_std::{
testing::{MockApi, MockStorage},
to_binary, Addr, Binary, Coin, Empty, StdResult, Uint128, Uint256,
};
use cw_multi_test::{
App, AppBuilder, AppResponse, BankKeeper, ContractWrapper, Executor, WasmKeeper,
};
use wormchain_accounting::{
msg::{
AllAccountsResponse, AllModificationsResponse, AllPendingTransfersResponse,
AllTransfersResponse, ExecuteMsg, Instantiate, InstantiateMsg, QueryMsg,
},
state,
};
use wormhole_bindings::{fake, WormholeQuery};
mod fake_tokenbridge;
pub struct Contract {
addr: Addr,
app: FakeApp,
}
impl Contract {
pub fn addr(&self) -> Addr {
self.addr.clone()
}
pub fn app(&self) -> &FakeApp {
&self.app
}
pub fn app_mut(&mut self) -> &mut FakeApp {
&mut self.app
}
pub fn submit_observations(
&mut self,
observations: Binary,
guardian_set_index: u32,
signature: wormhole_bindings::Signature,
) -> anyhow::Result<AppResponse> {
self.app.execute_contract(
Addr::unchecked(USER),
self.addr(),
&ExecuteMsg::SubmitObservations {
observations,
guardian_set_index,
signature,
},
&[],
)
}
pub fn modify_balance(
&mut self,
modification: Binary,
guardian_set_index: u32,
signatures: Vec<wormhole_bindings::Signature>,
) -> anyhow::Result<AppResponse> {
self.app.execute_contract(
Addr::unchecked(USER),
self.addr(),
&ExecuteMsg::ModifyBalance {
modification,
guardian_set_index,
signatures,
},
&[],
)
}
pub fn upgrade_contract(
&mut self,
upgrade: Binary,
guardian_set_index: u32,
signatures: Vec<wormhole_bindings::Signature>,
) -> anyhow::Result<AppResponse> {
self.app.execute_contract(
Addr::unchecked(ADMIN),
self.addr(),
&ExecuteMsg::UpgradeContract {
upgrade,
guardian_set_index,
signatures,
},
&[],
)
}
pub fn query_balance(&self, key: account::Key) -> StdResult<account::Balance> {
self.app
.wrap()
.query_wasm_smart(self.addr(), &QueryMsg::Balance(key))
}
pub fn query_all_accounts(
&self,
start_after: Option<account::Key>,
limit: Option<u32>,
) -> StdResult<AllAccountsResponse> {
self.app
.wrap()
.query_wasm_smart(self.addr(), &QueryMsg::AllAccounts { start_after, limit })
}
pub fn query_transfer(&self, key: transfer::Key) -> StdResult<transfer::Data> {
self.app
.wrap()
.query_wasm_smart(self.addr(), &QueryMsg::Transfer(key))
}
pub fn query_all_transfers(
&self,
start_after: Option<transfer::Key>,
limit: Option<u32>,
) -> StdResult<AllTransfersResponse> {
self.app
.wrap()
.query_wasm_smart(self.addr(), &QueryMsg::AllTransfers { start_after, limit })
}
pub fn query_pending_transfer(&self, key: transfer::Key) -> StdResult<Vec<state::Data>> {
self.app
.wrap()
.query_wasm_smart(self.addr(), &QueryMsg::PendingTransfer(key))
}
pub fn query_all_pending_transfers(
&self,
start_after: Option<transfer::Key>,
limit: Option<u32>,
) -> StdResult<AllPendingTransfersResponse> {
self.app.wrap().query_wasm_smart(
self.addr(),
&QueryMsg::AllPendingTransfers { start_after, limit },
)
}
pub fn query_modification(&self, sequence: u64) -> StdResult<Modification> {
self.app
.wrap()
.query_wasm_smart(self.addr(), &QueryMsg::Modification { sequence })
}
pub fn query_all_modifications(
&self,
start_after: Option<u64>,
limit: Option<u32>,
) -> StdResult<AllModificationsResponse> {
self.app.wrap().query_wasm_smart(
self.addr(),
&QueryMsg::AllModifications { start_after, limit },
)
}
}
const USER: &str = "USER";
const ADMIN: &str = "ADMIN";
const NATIVE_DENOM: &str = "denom";
pub type FakeApp =
App<BankKeeper, MockApi, MockStorage, fake::WormholeKeeper, WasmKeeper<Empty, WormholeQuery>>;
fn fake_app(wh: fake::WormholeKeeper) -> FakeApp {
AppBuilder::new_custom()
.with_custom(wh)
.build(|router, _, storage| {
router
.bank
.init_balance(
storage,
&Addr::unchecked(USER),
vec![Coin {
denom: NATIVE_DENOM.to_string(),
amount: Uint128::new(1),
}],
)
.unwrap();
})
}
pub fn create_accounts(count: usize) -> Vec<Account> {
let mut out = Vec::with_capacity(count * count);
for i in 0..count {
for j in 0..count {
let key = account::Key::new(i as u16, j as u16, [i as u8; 32].into());
let balance = Uint256::from(j as u128).into();
out.push(Account { key, balance });
}
}
out
}
pub fn create_transfers(count: usize) -> Vec<Transfer> {
let mut out = Vec::with_capacity(count);
for i in 0..count {
let key = transfer::Key::new(i as u16, [i as u8; 32].into(), i as u64);
let data = transfer::Data {
amount: Uint256::from(i as u128),
token_chain: i as u16,
token_address: [i as u8; 32].into(),
recipient_chain: i as u16,
};
out.push(Transfer { key, data });
}
out
}
pub fn create_modifications(count: usize) -> Vec<Modification> {
let mut out = Vec::with_capacity(count);
for i in 0..count {
let m = Modification {
sequence: i as u64,
chain_id: i as u16,
token_chain: i as u16,
token_address: [i as u8; 32].into(),
kind: if i % 2 == 0 { Kind::Add } else { Kind::Sub },
amount: Uint256::from(i as u128),
reason: format!("{i}"),
};
out.push(m);
}
out
}
pub fn proper_instantiate(
accounts: Vec<Account>,
transfers: Vec<Transfer>,
modifications: Vec<Modification>,
) -> (fake::WormholeKeeper, Contract) {
let wh = fake::WormholeKeeper::new();
let mut app = fake_app(wh.clone());
let tokenbridge_id = app.store_code(Box::new(ContractWrapper::new_with_empty(
fake_tokenbridge::execute,
fake_tokenbridge::instantiate,
fake_tokenbridge::query,
)));
let accounting_id = app.store_code(Box::new(ContractWrapper::new(
wormchain_accounting::contract::execute,
wormchain_accounting::contract::instantiate,
wormchain_accounting::contract::query,
)));
let tokenbridge_addr = app
.instantiate_contract(
tokenbridge_id,
Addr::unchecked(ADMIN),
&Empty {},
&[],
"tokenbridge",
None,
)
.unwrap()
.into();
let instantiate = to_binary(&Instantiate {
tokenbridge_addr,
accounts,
transfers,
modifications,
})
.unwrap();
let signatures = wh.sign(&instantiate);
let msg = InstantiateMsg {
instantiate,
guardian_set_index: wh.guardian_set_index(),
signatures,
};
// We want the contract to be able to upgrade itself, which means we have to set the contract
// as its own admin. So we have a bit of a catch-22 where we need to know the contract
// address to register it but we need to register it to get its address. The hacky solution
// here is to rely on the internal details of the test framework to figure out what the
// address of the contract is going to be and then use that.
//
// TODO: Figure out a better way to do this. One option is to do something like:
//
// ```
// let mut data = app.contract_data(&addr).unwrap();
// data.admin = Some(addr.clone());
// app.init_modules(|router, _, storage| router.wasm.save_contract(storage, &addr, &data))
// .unwrap();
// ```
//
// Unfortunately, the `wasm` field of `router` is private to the `cw-multi-test` crate so we
// can't use it here. Maybe something to bring up with upstream.
let addr = app
.instantiate_contract(
accounting_id,
Addr::unchecked(ADMIN),
&msg,
&[],
"accounting",
Some("contract1".into()),
)
.unwrap();
(wh, Contract { addr, app })
}

View File

@ -0,0 +1,29 @@
mod helpers;
use helpers::*;
#[test]
fn instantiate_contract() {
const COUNT: usize = 5;
let accounts = create_accounts(COUNT);
let transfers = create_transfers(COUNT);
let modifications = create_modifications(COUNT);
let (_, contract) =
proper_instantiate(accounts.clone(), transfers.clone(), modifications.clone());
for a in accounts {
let balance = contract.query_balance(a.key).unwrap();
assert_eq!(a.balance, balance);
}
for t in transfers {
let data = contract.query_transfer(t.key).unwrap();
assert_eq!(t.data, data);
}
for m in modifications {
let data = contract.query_modification(m.sequence).unwrap();
assert_eq!(m, data);
}
}

View File

@ -0,0 +1,243 @@
mod helpers;
use accounting::state::{account, Kind, Modification};
use cosmwasm_std::{to_binary, Event, Uint256};
use helpers::*;
#[test]
fn simple_modify() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let index = wh.guardian_set_index();
let m = Modification {
sequence: 0,
chain_id: 1,
token_chain: 1,
token_address: [0x7c; 32].into(),
kind: Kind::Add,
amount: Uint256::from(300u128),
reason: "test".into(),
};
let modification = to_binary(&m).unwrap();
let signatures = wh.sign(&modification);
let resp = contract
.modify_balance(modification, index, signatures)
.unwrap();
let evt = Event::new("wasm-ModifyBalance")
.add_attribute("sequence", m.sequence.to_string())
.add_attribute("chain_id", m.chain_id.to_string())
.add_attribute("token_chain", m.token_chain.to_string())
.add_attribute("token_address", m.token_address.to_string())
.add_attribute("kind", m.kind.to_string())
.add_attribute("amount", m.amount)
.add_attribute("reason", m.reason.clone());
resp.assert_event(&evt);
let actual = contract.query_modification(m.sequence).unwrap();
assert_eq!(m, actual);
let balance = contract
.query_balance(account::Key::new(
m.chain_id,
m.token_chain,
m.token_address,
))
.unwrap();
assert_eq!(m.amount, *balance);
}
#[test]
fn duplicate_modify() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let index = wh.guardian_set_index();
let m = Modification {
sequence: 0,
chain_id: 1,
token_chain: 1,
token_address: [0x7c; 32].into(),
kind: Kind::Add,
amount: Uint256::from(300u128),
reason: "test".into(),
};
let modification = to_binary(&m).unwrap();
let signatures = wh.sign(&modification);
contract
.modify_balance(modification.clone(), index, signatures.clone())
.unwrap();
contract
.modify_balance(modification, index, signatures)
.expect_err("successfully submitted duplicate modification");
}
#[test]
fn round_trip() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let index = wh.guardian_set_index();
let mut m = Modification {
sequence: 0,
chain_id: 1,
token_chain: 1,
token_address: [0x7c; 32].into(),
kind: Kind::Add,
amount: Uint256::from(300u128),
reason: "test".into(),
};
let modification = to_binary(&m).unwrap();
let signatures = wh.sign(&modification);
contract
.modify_balance(modification, index, signatures)
.unwrap();
let actual = contract.query_modification(m.sequence).unwrap();
assert_eq!(m, actual);
// Now reverse the modification.
m.sequence += 1;
m.kind = Kind::Sub;
m.reason = "reverse".into();
let modification = to_binary(&m).unwrap();
let signatures = wh.sign(&modification);
contract
.modify_balance(modification, index, signatures)
.unwrap();
let actual = contract.query_modification(m.sequence).unwrap();
assert_eq!(m, actual);
let balance = contract
.query_balance(account::Key::new(
m.chain_id,
m.token_chain,
m.token_address,
))
.unwrap();
assert_eq!(Uint256::zero(), *balance);
}
#[test]
fn missing_guardian_set() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let index = wh.guardian_set_index();
let m = Modification {
sequence: 0,
chain_id: 1,
token_chain: 1,
token_address: [0x7c; 32].into(),
kind: Kind::Add,
amount: Uint256::from(300u128),
reason: "test".into(),
};
let modification = to_binary(&m).unwrap();
let signatures = wh.sign(&modification);
contract
.modify_balance(modification, index + 1, signatures)
.expect_err("successfully modified balance with invalid guardian set");
}
#[test]
fn expired_guardian_set() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let index = wh.guardian_set_index();
let mut block = contract.app().block_info();
wh.set_expiration(block.height);
block.height += 1;
contract.app_mut().set_block(block);
let m = Modification {
sequence: 0,
chain_id: 1,
token_chain: 1,
token_address: [0x7c; 32].into(),
kind: Kind::Add,
amount: Uint256::from(300u128),
reason: "test".into(),
};
let modification = to_binary(&m).unwrap();
let signatures = wh.sign(&modification);
contract
.modify_balance(modification, index, signatures)
.expect_err("successfully modified balance with expired guardian set");
}
#[test]
fn no_quorum() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let index = wh.guardian_set_index();
let m = Modification {
sequence: 0,
chain_id: 1,
token_chain: 1,
token_address: [0x7c; 32].into(),
kind: Kind::Add,
amount: Uint256::from(300u128),
reason: "test".into(),
};
let modification = to_binary(&m).unwrap();
let mut signatures = wh.sign(&modification);
let newlen = wh
.calculate_quorum(0, contract.app().block_info().height)
.map(|q| (q - 1) as usize)
.unwrap();
signatures.truncate(newlen);
contract
.modify_balance(modification, index, signatures)
.expect_err("successfully submitted modification without quorum");
}
#[test]
fn repeat() {
const ITERATIONS: usize = 10;
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let index = wh.guardian_set_index();
let mut m = Modification {
sequence: 0,
chain_id: 1,
token_chain: 1,
token_address: [0x7c; 32].into(),
kind: Kind::Add,
amount: Uint256::from(300u128),
reason: "test".into(),
};
for _ in 0..ITERATIONS {
m.sequence += 1;
let modification = to_binary(&m).unwrap();
let signatures = wh.sign(&modification);
contract
.modify_balance(modification, index, signatures)
.unwrap();
let actual = contract.query_modification(m.sequence).unwrap();
assert_eq!(m, actual);
}
let balance = contract
.query_balance(account::Key::new(
m.chain_id,
m.token_chain,
m.token_address,
))
.unwrap();
assert_eq!(m.amount * Uint256::from(ITERATIONS as u128), *balance);
}

View File

@ -0,0 +1,276 @@
mod helpers;
use std::collections::BTreeMap;
use accounting::state::{
account::{self, Balance},
transfer, Kind, Modification,
};
use cosmwasm_std::Uint256;
use helpers::*;
use wormhole_bindings::fake;
fn set_up(count: usize) -> (fake::WormholeKeeper, Contract) {
let accounts = create_accounts(count);
let transfers = create_transfers(count);
let modifications = create_modifications(count);
proper_instantiate(accounts, transfers, modifications)
}
#[test]
fn account_balance() {
let count = 2;
let (_, contract) = set_up(count);
for i in 0..count {
for j in 0..count {
let key = account::Key::new(i as u16, j as u16, [i as u8; 32].into());
let balance = contract.query_balance(key).unwrap();
assert_eq!(balance, Balance::new(Uint256::from(j as u128)))
}
}
}
#[test]
fn missing_account() {
let count = 2;
let (_, contract) = set_up(count);
let missing = account::Key::new(
(count + 1) as u16,
(count + 2) as u16,
[(count + 3) as u8; 32].into(),
);
contract
.query_balance(missing)
.expect_err("successfully queried missing account key");
}
#[test]
fn all_balances() {
let count = 3;
let (_, contract) = set_up(count);
let resp = contract.query_all_accounts(None, None).unwrap();
let found = resp
.accounts
.into_iter()
.map(|acc| (acc.key, acc.balance))
.collect::<BTreeMap<_, _>>();
assert_eq!(found.len(), count * count);
for i in 0..count {
for j in 0..count {
let key = account::Key::new(i as u16, j as u16, [i as u8; 32].into());
assert!(found.contains_key(&key));
}
}
}
#[test]
fn all_balances_sub_range() {
let count = 3;
let (_, contract) = set_up(count);
for i in 0..count {
for j in 0..count {
let max_limit = (count - i - 1) * count + (count - j - 1);
for l in 1..=max_limit {
let start_after = Some(account::Key::new(i as u16, j as u16, [i as u8; 32].into()));
let limit = Some(l as u32);
let resp = contract.query_all_accounts(start_after, limit).unwrap();
let found = resp
.accounts
.into_iter()
.map(|acc| (acc.key, acc.balance))
.collect::<BTreeMap<_, _>>();
assert_eq!(found.len(), l);
let mut checked = 0;
for y in j + 1..count {
if checked >= l {
break;
}
let key = account::Key::new(i as u16, y as u16, [i as u8; 32].into());
assert!(found.contains_key(&key));
checked += 1;
}
'outer: for x in i + 1..count {
for y in 0..count {
if checked >= l {
break 'outer;
}
let key = account::Key::new(x as u16, y as u16, [x as u8; 32].into());
assert!(found.contains_key(&key));
checked += 1;
}
}
}
}
}
}
#[test]
fn transfer_data() {
let count = 2;
let (_, contract) = set_up(count);
for i in 0..count {
let expected = transfer::Data {
amount: Uint256::from(i as u128),
token_chain: i as u16,
token_address: [i as u8; 32].into(),
recipient_chain: i as u16,
};
let key = transfer::Key::new(i as u16, [i as u8; 32].into(), i as u64);
let actual = contract.query_transfer(key).unwrap();
assert_eq!(expected, actual);
}
}
#[test]
fn missing_transfer() {
let count = 2;
let (_, contract) = set_up(count);
let missing = transfer::Key::new(
(count + 1) as u16,
[(count + 2) as u8; 32].into(),
(count + 3) as u64,
);
contract
.query_transfer(missing)
.expect_err("successfully queried missing transfer key");
}
#[test]
fn all_transfer_data() {
let count = 3;
let (_, contract) = set_up(count);
let resp = contract.query_all_transfers(None, None).unwrap();
let found = resp
.transfers
.into_iter()
.map(|acc| (acc.key, acc.data))
.collect::<BTreeMap<_, _>>();
assert_eq!(found.len(), count);
for i in 0..count {
let key = transfer::Key::new(i as u16, [i as u8; 32].into(), i as u64);
assert!(found.contains_key(&key));
}
}
#[test]
fn all_transfer_data_sub_range() {
let count = 5;
let (_, contract) = set_up(count);
for i in 0..count {
for l in 1..count - i {
let start_after = Some(transfer::Key::new(i as u16, [i as u8; 32].into(), i as u64));
let limit = Some(l as u32);
let resp = contract.query_all_transfers(start_after, limit).unwrap();
let found = resp
.transfers
.into_iter()
.map(|acc| (acc.key, acc.data))
.collect::<BTreeMap<_, _>>();
assert_eq!(found.len(), l);
for x in i + 1..=i + l {
let key = transfer::Key::new(x as u16, [x as u8; 32].into(), x as u64);
assert!(found.contains_key(&key));
}
}
}
}
#[test]
fn modification_data() {
let count = 2;
let (_, contract) = set_up(count);
for i in 0..count {
let expected = Modification {
sequence: i as u64,
chain_id: i as u16,
token_chain: i as u16,
token_address: [i as u8; 32].into(),
kind: if i % 2 == 0 { Kind::Add } else { Kind::Sub },
amount: Uint256::from(i as u128),
reason: format!("{i}"),
};
let key = i as u64;
let actual = contract.query_modification(key).unwrap();
assert_eq!(expected, actual);
}
}
#[test]
fn missing_modification() {
let count = 2;
let (_, contract) = set_up(count);
let missing = (count + 1) as u64;
contract
.query_modification(missing)
.expect_err("successfully queried missing modification key");
}
#[test]
fn all_modification_data() {
let count = 3;
let (_, contract) = set_up(count);
let resp = contract.query_all_modifications(None, None).unwrap();
let found = resp
.modifications
.into_iter()
.map(|m| (m.sequence, m))
.collect::<BTreeMap<_, _>>();
assert_eq!(found.len(), count);
for i in 0..count {
let key = i as u64;
assert!(found.contains_key(&key));
}
}
#[test]
fn all_modification_data_sub_range() {
let count = 5;
let (_, contract) = set_up(count);
for i in 0..count {
for l in 1..count - i {
let start_after = Some(i as u64);
let limit = Some(l as u32);
let resp = contract
.query_all_modifications(start_after, limit)
.unwrap();
let found = resp
.modifications
.into_iter()
.map(|m| (m.sequence, m))
.collect::<BTreeMap<_, _>>();
assert_eq!(found.len(), l);
for x in i + 1..=i + l {
let key = x as u64;
assert!(found.contains_key(&key));
}
}
}
}

View File

@ -0,0 +1,755 @@
mod helpers;
use accounting::state::{
account::{self, Balance},
transfer, Account, TokenAddress,
};
use cosmwasm_std::{to_binary, Binary, Event, Uint256};
use cw_multi_test::AppResponse;
use helpers::*;
use wormchain_accounting::msg::Observation;
use wormhole::{token::Message, Address, Amount};
use wormhole_bindings::fake;
fn set_up(count: usize) -> (Vec<Message>, Vec<Observation>) {
let mut txs = Vec::with_capacity(count);
let mut observations = Vec::with_capacity(count);
for i in 0..count {
let key = transfer::Key::new(i as u16, [i as u8; 32].into(), i as u64);
let tx = Message::Transfer {
amount: Amount(Uint256::from(500u128).to_be_bytes()),
token_address: Address([(i + 1) as u8; 32]),
token_chain: (i as u16).into(),
recipient: Address([(i + 2) as u8; 32]),
recipient_chain: ((i + 3) as u16).into(),
fee: Amount([0u8; 32]),
};
let payload = serde_wormhole::to_vec(&tx).map(Binary::from).unwrap();
txs.push(tx);
observations.push(Observation {
key,
nonce: i as u32,
tx_hash: vec![(i + 4) as u8; 20].into(),
payload,
});
}
(txs, observations)
}
#[test]
fn batch() {
const COUNT: usize = 5;
let (txs, observations) = set_up(COUNT);
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let index = wh.guardian_set_index();
let obs = to_binary(&observations).unwrap();
let signatures = wh.sign(&obs);
let quorum = wh
.calculate_quorum(index, contract.app().block_info().height)
.unwrap() as usize;
for (i, s) in signatures.into_iter().enumerate() {
if i < quorum {
contract.submit_observations(obs.clone(), index, s).unwrap();
// Once there is a quorum the pending transfers are removed.
if i < quorum - 1 {
for o in &observations {
let data = contract.query_pending_transfer(o.key.clone()).unwrap();
assert_eq!(o, data[0].observation());
// Make sure the transfer hasn't yet been committed.
contract
.query_transfer(o.key.clone())
.expect_err("transfer committed without quorum");
}
} else {
for o in &observations {
contract
.query_pending_transfer(o.key.clone())
.expect_err("found pending transfer for observation with quorum");
}
}
} else {
contract
.submit_observations(obs.clone(), index, s)
.expect_err("successfully submitted observation for committed transfer");
}
}
for (tx, o) in txs.into_iter().zip(observations) {
let expected = if let Message::Transfer {
amount,
token_address,
token_chain,
recipient_chain,
..
} = tx
{
transfer::Data {
amount: Uint256::new(amount.0),
token_chain: token_chain.into(),
token_address: TokenAddress::new(token_address.0),
recipient_chain: recipient_chain.into(),
}
} else {
panic!("unexpected tokenbridge payload");
};
let emitter_chain = o.key.emitter_chain();
let actual = contract.query_transfer(o.key).unwrap();
assert_eq!(expected, actual);
let src = contract
.query_balance(account::Key::new(
emitter_chain,
expected.token_chain,
expected.token_address,
))
.unwrap();
assert_eq!(expected.amount, *src);
let dst = contract
.query_balance(account::Key::new(
expected.recipient_chain,
expected.token_chain,
expected.token_address,
))
.unwrap();
assert_eq!(expected.amount, *dst);
}
}
#[test]
fn duplicates() {
const COUNT: usize = 5;
let (txs, observations) = set_up(COUNT);
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let index = wh.guardian_set_index();
let obs = to_binary(&observations).unwrap();
let signatures = wh.sign(&obs);
let quorum = wh
.calculate_quorum(index, contract.app().block_info().height)
.unwrap() as usize;
for (i, s) in signatures.iter().take(quorum).cloned().enumerate() {
contract
.submit_observations(obs.clone(), index, s.clone())
.unwrap();
let err = contract
.submit_observations(obs.clone(), index, s)
.expect_err("successfully submitted duplicate observations");
if i < quorum - 1 {
// Sadly we can't match on the exact error type in an integration test because the
// test frameworks converts it into a string before it reaches this point.
assert!(format!("{err:#}").contains("duplicate signatures"));
}
}
for (tx, o) in txs.into_iter().zip(observations) {
let expected = if let Message::Transfer {
amount,
token_address,
token_chain,
recipient_chain,
..
} = tx
{
transfer::Data {
amount: Uint256::new(amount.0),
token_chain: token_chain.into(),
token_address: TokenAddress::new(token_address.0),
recipient_chain: recipient_chain.into(),
}
} else {
panic!("unexpected tokenbridge payload");
};
let emitter_chain = o.key.emitter_chain();
let actual = contract.query_transfer(o.key).unwrap();
assert_eq!(expected, actual);
let src = contract
.query_balance(account::Key::new(
emitter_chain,
expected.token_chain,
expected.token_address,
))
.unwrap();
assert_eq!(expected.amount, *src);
let dst = contract
.query_balance(account::Key::new(
expected.recipient_chain,
expected.token_chain,
expected.token_address,
))
.unwrap();
assert_eq!(expected.amount, *dst);
}
for s in signatures {
contract
.submit_observations(obs.clone(), index, s)
.expect_err("successfully submitted observation for committed transfer");
}
}
fn transfer_tokens(
wh: &fake::WormholeKeeper,
contract: &mut Contract,
key: transfer::Key,
msg: Message,
index: u32,
quorum: usize,
) -> anyhow::Result<(Observation, Vec<AppResponse>)> {
let payload = serde_wormhole::to_vec(&msg).map(Binary::from).unwrap();
let o = Observation {
key,
nonce: 0x4343b191,
tx_hash: vec![0xd8u8; 20].into(),
payload,
};
let obs = to_binary(&vec![o.clone()]).unwrap();
let signatures = wh.sign(&obs);
let responses = signatures
.into_iter()
.take(quorum)
.map(|s| contract.submit_observations(obs.clone(), index, s))
.collect::<anyhow::Result<Vec<_>>>()?;
Ok((o, responses))
}
#[test]
fn round_trip() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let index = wh.guardian_set_index();
let quorum = wh
.calculate_quorum(index, contract.app().block_info().height)
.unwrap() as usize;
let emitter_chain = 2;
let amount = Amount(Uint256::from(500u128).to_be_bytes());
let token_address = Address([0xccu8; 32]);
let token_chain = 2u16.into();
let recipient_chain = 14u16.into();
let key = transfer::Key::new(emitter_chain, [emitter_chain as u8; 32].into(), 37);
let msg = Message::Transfer {
amount,
token_address,
token_chain,
recipient: Address([0xb9u8; 32]),
recipient_chain,
fee: Amount([0u8; 32]),
};
transfer_tokens(&wh, &mut contract, key.clone(), msg, index, quorum).unwrap();
let expected = transfer::Data {
amount: Uint256::new(amount.0),
token_chain: token_chain.into(),
token_address: TokenAddress::new(token_address.0),
recipient_chain: recipient_chain.into(),
};
let actual = contract.query_transfer(key).unwrap();
assert_eq!(expected, actual);
// Now send the tokens back.
let key = transfer::Key::new(
recipient_chain.into(),
[u16::from(recipient_chain) as u8; 32].into(),
91156748,
);
let msg = Message::Transfer {
amount,
token_address,
token_chain,
recipient: Address([0xe4u8; 32]),
recipient_chain: emitter_chain.into(),
fee: Amount([0u8; 32]),
};
transfer_tokens(&wh, &mut contract, key.clone(), msg, index, quorum).unwrap();
let expected = transfer::Data {
amount: Uint256::new(amount.0),
token_chain: token_chain.into(),
token_address: TokenAddress::new(token_address.0),
recipient_chain: emitter_chain,
};
let actual = contract.query_transfer(key).unwrap();
assert_eq!(expected, actual);
// Now both balances should be zero.
let src = contract
.query_balance(account::Key::new(
emitter_chain,
token_chain.into(),
expected.token_address,
))
.unwrap();
assert_eq!(Uint256::zero(), *src);
let dst = contract
.query_balance(account::Key::new(
recipient_chain.into(),
token_chain.into(),
expected.token_address,
))
.unwrap();
assert_eq!(Uint256::zero(), *dst);
}
#[test]
fn missing_guardian_set() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let index = wh.guardian_set_index();
let quorum = wh
.calculate_quorum(index, contract.app().block_info().height)
.unwrap() as usize;
let emitter_chain = 2;
let amount = Amount(Uint256::from(500u128).to_be_bytes());
let token_address = Address([0xccu8; 32]);
let token_chain = 2.into();
let recipient_chain = 14.into();
let key = transfer::Key::new(emitter_chain, [emitter_chain as u8; 32].into(), 37);
let msg = Message::Transfer {
amount,
token_address,
token_chain,
recipient: Address([0xb9u8; 32]),
recipient_chain,
fee: Amount([0u8; 32]),
};
transfer_tokens(&wh, &mut contract, key, msg, index + 1, quorum)
.expect_err("successfully submitted observations with invalid guardian set");
}
#[test]
fn expired_guardian_set() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let index = wh.guardian_set_index();
let mut block = contract.app().block_info();
let quorum = wh.calculate_quorum(index, block.height).unwrap() as usize;
let emitter_chain = 2;
let amount = Amount(Uint256::from(500u128).to_be_bytes());
let token_address = Address([0xccu8; 32]);
let token_chain = 2.into();
let recipient_chain = 14.into();
let key = transfer::Key::new(emitter_chain, [emitter_chain as u8; 32].into(), 37);
let msg = Message::Transfer {
amount,
token_address,
token_chain,
recipient: Address([0xb9u8; 32]),
recipient_chain,
fee: Amount([0u8; 32]),
};
// Mark the guardian set expired.
wh.set_expiration(block.height);
block.height += 1;
contract.app_mut().set_block(block);
transfer_tokens(&wh, &mut contract, key, msg, index, quorum)
.expect_err("successfully submitted observations with expired guardian set");
}
#[test]
fn no_quorum() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let index = wh.guardian_set_index();
let quorum = wh
.calculate_quorum(index, contract.app().block_info().height)
.unwrap() as usize;
let emitter_chain = 2;
let amount = Amount(Uint256::from(500u128).to_be_bytes());
let token_address = Address([0xccu8; 32]);
let token_chain = 2.into();
let recipient_chain = 14.into();
let key = transfer::Key::new(emitter_chain, [emitter_chain as u8; 32].into(), 37);
let msg = Message::Transfer {
amount,
token_address,
token_chain,
recipient: Address([0xb9u8; 32]),
recipient_chain,
fee: Amount([0u8; 32]),
};
transfer_tokens(
&wh,
&mut contract,
key.clone(),
msg.clone(),
index,
quorum - 1,
)
.unwrap();
let data = contract.query_pending_transfer(key.clone()).unwrap();
assert_eq!(key, data[0].observation().key);
let actual = serde_wormhole::from_slice(&data[0].observation().payload).unwrap();
assert_eq!(msg, actual);
// Make sure the transfer hasn't yet been committed.
contract
.query_transfer(key)
.expect_err("transfer committed without quorum");
}
#[test]
fn missing_wrapped_account() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let index = wh.guardian_set_index();
let quorum = wh
.calculate_quorum(index, contract.app().block_info().height)
.unwrap() as usize;
let emitter_chain = 14;
let amount = Amount(Uint256::from(500u128).to_be_bytes());
let token_address = Address([0xccu8; 32]);
let token_chain = 2.into();
let recipient_chain = 2.into();
let key = transfer::Key::new(emitter_chain, [emitter_chain as u8; 32].into(), 37);
let msg = Message::Transfer {
amount,
token_address,
token_chain,
recipient: Address([0xb9u8; 32]),
recipient_chain,
fee: Amount([0u8; 32]),
};
transfer_tokens(&wh, &mut contract, key, msg, index, quorum)
.expect_err("successfully burned wrapped tokens without a wrapped amount");
}
#[test]
fn missing_native_account() {
let emitter_chain = 14;
let recipient_chain = 2;
let amount = Amount(Uint256::from(500u128).to_be_bytes());
let token_address = [0xccu8; 32];
let token_chain = 2;
// We need to set up a fake wrapped account so that the initial check succeeds.
let (wh, mut contract) = proper_instantiate(
vec![Account {
key: account::Key::new(emitter_chain, token_chain, token_address.into()),
balance: Balance::new(Uint256::new(amount.0)),
}],
Vec::new(),
Vec::new(),
);
let index = wh.guardian_set_index();
let quorum = wh
.calculate_quorum(index, contract.app().block_info().height)
.unwrap() as usize;
let key = transfer::Key::new(emitter_chain, [emitter_chain as u8; 32].into(), 37);
let msg = Message::Transfer {
amount,
token_address: Address(token_address),
token_chain: token_chain.into(),
recipient: Address([0xb9u8; 32]),
recipient_chain: recipient_chain.into(),
fee: Amount([0u8; 32]),
};
transfer_tokens(&wh, &mut contract, key, msg, index, quorum)
.expect_err("successfully unlocked native tokens without a native account");
}
#[test]
fn repeated() {
const ITERATIONS: usize = 10;
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let index = wh.guardian_set_index();
let quorum = wh
.calculate_quorum(index, contract.app().block_info().height)
.unwrap() as usize;
let emitter_chain = 2;
let recipient_chain = 14;
let amount = Amount(Uint256::from(500u128).to_be_bytes());
let token_address = [0xccu8; 32];
let token_chain = 2;
let msg = Message::Transfer {
amount,
token_address: Address(token_address),
token_chain: token_chain.into(),
recipient: Address([0xb9u8; 32]),
recipient_chain: recipient_chain.into(),
fee: Amount([0u8; 32]),
};
for i in 0..ITERATIONS {
let key = transfer::Key::new(emitter_chain, [emitter_chain as u8; 32].into(), i as u64);
transfer_tokens(&wh, &mut contract, key.clone(), msg.clone(), index, quorum).unwrap();
}
let expected = Uint256::new(amount.0) * Uint256::from(ITERATIONS as u128);
let src = contract
.query_balance(account::Key::new(
emitter_chain,
token_chain,
token_address.into(),
))
.unwrap();
assert_eq!(expected, *src);
let dst = contract
.query_balance(account::Key::new(
recipient_chain,
token_chain,
token_address.into(),
))
.unwrap();
assert_eq!(expected, *dst);
}
#[test]
fn wrapped_to_wrapped() {
let emitter_chain = 14;
let recipient_chain = 2;
let amount = Amount(Uint256::from(500u128).to_be_bytes());
let token_address = [0xccu8; 32];
let token_chain = 5;
// We need an initial fake wrapped account.
let (wh, mut contract) = proper_instantiate(
vec![Account {
key: account::Key::new(emitter_chain, token_chain, token_address.into()),
balance: Balance::new(Uint256::new(amount.0)),
}],
Vec::new(),
Vec::new(),
);
let index = wh.guardian_set_index();
let quorum = wh
.calculate_quorum(index, contract.app().block_info().height)
.unwrap() as usize;
let key = transfer::Key::new(emitter_chain, [emitter_chain as u8; 32].into(), 37);
let msg = Message::Transfer {
amount,
token_address: Address(token_address),
token_chain: token_chain.into(),
recipient: Address([0xb9u8; 32]),
recipient_chain: recipient_chain.into(),
fee: Amount([0u8; 32]),
};
transfer_tokens(&wh, &mut contract, key.clone(), msg, index, quorum).unwrap();
let expected = transfer::Data {
amount: Uint256::new(amount.0),
token_chain,
token_address: TokenAddress::new(token_address),
recipient_chain,
};
let actual = contract.query_transfer(key).unwrap();
assert_eq!(expected, actual);
let src = contract
.query_balance(account::Key::new(
emitter_chain,
token_chain,
token_address.into(),
))
.unwrap();
assert_eq!(Uint256::zero(), *src);
let dst = contract
.query_balance(account::Key::new(
recipient_chain,
token_chain,
token_address.into(),
))
.unwrap();
assert_eq!(Uint256::new(amount.0), *dst);
}
#[test]
fn unknown_emitter() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let index = wh.guardian_set_index();
let quorum = wh
.calculate_quorum(index, contract.app().block_info().height)
.unwrap() as usize;
let emitter_chain = 14;
let amount = Amount(Uint256::from(500u128).to_be_bytes());
let token_address = Address([0xccu8; 32]);
let token_chain = 2.into();
let recipient_chain = 2.into();
let key = transfer::Key::new(emitter_chain, [0xde; 32].into(), 37);
let msg = Message::Transfer {
amount,
token_address,
token_chain,
recipient: Address([0xb9u8; 32]),
recipient_chain,
fee: Amount([0u8; 32]),
};
transfer_tokens(&wh, &mut contract, key, msg, index, quorum)
.expect_err("successfully transfered tokens with an invalid emitter address");
}
#[test]
fn different_observations() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let index = wh.guardian_set_index();
let quorum = wh
.calculate_quorum(index, contract.app().block_info().height)
.unwrap() as usize;
// First submit some observations without enough signatures for quorum.
let emitter_chain = 2;
let fake_amount = Amount(Uint256::from(500u128).to_be_bytes());
let token_address = Address([0xccu8; 32]);
let token_chain = 2.into();
let fake_recipient_chain = 14.into();
let key = transfer::Key::new(emitter_chain, [emitter_chain as u8; 32].into(), 37);
let fake = Message::Transfer {
amount: fake_amount,
token_address,
token_chain,
recipient: Address([0xb9u8; 32]),
recipient_chain: fake_recipient_chain,
fee: Amount([0u8; 32]),
};
transfer_tokens(&wh, &mut contract, key.clone(), fake, index, quorum - 1).unwrap();
// Make sure there is no committed transfer yet.
contract
.query_transfer(key.clone())
.expect_err("committed transfer without quorum");
// Now change the details of the transfer and resubmit with the same key.
let real_amount = Amount(Uint256::from(200u128).to_be_bytes());
let real_recipient_chain = 9.into();
let real = Message::Transfer {
amount: real_amount,
token_address,
token_chain,
recipient: Address([0xb9u8; 32]),
recipient_chain: real_recipient_chain,
fee: Amount([0u8; 32]),
};
transfer_tokens(&wh, &mut contract, key.clone(), real, index, quorum).unwrap();
contract
.query_pending_transfer(key.clone())
.expect_err("found pending transfer for observation with quorum");
let expected = transfer::Data {
amount: Uint256::new(real_amount.0),
token_chain: token_chain.into(),
token_address: TokenAddress::new(token_address.0),
recipient_chain: real_recipient_chain.into(),
};
let actual = contract.query_transfer(key).unwrap();
assert_eq!(expected, actual);
let src = contract
.query_balance(account::Key::new(
emitter_chain,
token_chain.into(),
expected.token_address,
))
.unwrap();
assert_eq!(Uint256::new(real_amount.0), *src);
let dst = contract
.query_balance(account::Key::new(
real_recipient_chain.into(),
token_chain.into(),
expected.token_address,
))
.unwrap();
assert_eq!(Uint256::new(real_amount.0), *dst);
}
#[test]
fn emit_event_with_quorum() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let index = wh.guardian_set_index();
let quorum = wh
.calculate_quorum(index, contract.app().block_info().height)
.unwrap() as usize;
let emitter_chain = 2;
let amount = Amount(Uint256::from(500u128).to_be_bytes());
let token_address = Address([0xccu8; 32]);
let token_chain = 2.into();
let recipient_chain = 14.into();
let key = transfer::Key::new(emitter_chain, [emitter_chain as u8; 32].into(), 37);
let msg = Message::Transfer {
amount,
token_address,
token_chain,
recipient: Address([0xb9u8; 32]),
recipient_chain,
fee: Amount([0u8; 32]),
};
let (o, responses) = transfer_tokens(&wh, &mut contract, key, msg, index, quorum).unwrap();
let expected = Event::new("wasm-Transfer")
.add_attribute("emitter_chain", o.key.emitter_chain().to_string())
.add_attribute("emitter_address", o.key.emitter_address().to_string())
.add_attribute("sequence", o.key.sequence().to_string())
.add_attribute("nonce", o.nonce.to_string())
.add_attribute("tx_hash", o.tx_hash.to_base64())
.add_attribute("payload", o.payload.to_base64());
assert_eq!(responses.len(), quorum);
for (i, r) in responses.into_iter().enumerate() {
if i < quorum - 1 {
assert!(!r.has_event(&expected));
} else {
r.assert_event(&expected);
}
}
}

View File

@ -0,0 +1,60 @@
mod helpers;
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{
to_binary, Binary, Deps, DepsMut, Empty, Env, Event, MessageInfo, Response, StdResult,
};
use cw_multi_test::ContractWrapper;
use helpers::*;
use wormchain_accounting::msg::Upgrade;
use wormhole_bindings::WormholeQuery;
pub fn instantiate(
_deps: DepsMut,
_env: Env,
_info: MessageInfo,
_msg: Empty,
) -> StdResult<Response> {
Ok(Response::default())
}
pub fn migrate(_deps: DepsMut<WormholeQuery>, _env: Env, _msg: Empty) -> StdResult<Response> {
Ok(Response::default().add_event(Event::new("migrate-success")))
}
pub fn execute(_deps: DepsMut, _env: Env, _info: MessageInfo, _msg: Empty) -> StdResult<Response> {
Ok(Response::default())
}
#[cw_serde]
struct NewContract;
pub fn query(_deps: Deps, _env: Env, _msg: Empty) -> StdResult<Binary> {
to_binary(&NewContract)
}
#[test]
fn upgrade() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let new_code_id = contract.app_mut().store_code(Box::new(
ContractWrapper::new_with_empty(execute, instantiate, query).with_migrate_empty(migrate),
));
let mut new_addr = [0u8; 32];
new_addr[24..].copy_from_slice(&new_code_id.to_be_bytes());
let upgrade = to_binary(&Upgrade { new_addr }).unwrap();
let signatures = wh.sign(&upgrade);
let resp = contract
.upgrade_contract(upgrade, wh.guardian_set_index(), signatures)
.unwrap();
resp.assert_event(&Event::new("wasm-migrate-success"));
contract
.app()
.wrap()
.query_wasm_smart::<NewContract>(contract.addr(), &Empty {})
.unwrap();
}

View File

@ -125,7 +125,7 @@ impl<'de> de::Visitor<'de> for Base64Visitor {
E: de::Error,
{
base64::decode(v)
.map_err(|_| E::invalid_value(de::Unexpected::Str(v), &self))
.map_err(E::custom)
.and_then(|b| {
b.try_into()
.map_err(|b: Vec<u8>| E::invalid_length(b.len(), &self))

View File

@ -4,9 +4,14 @@ version = "0.1.0"
authors = ["Wormhole Project Contributors"]
edition = "2021"
[features]
fake = ["dep:cw-multi-test", "dep:k256"]
[dependencies]
anyhow = "1"
cosmwasm-schema = "1"
cosmwasm-std = "1"
schemars = "0.8.8"
serde = { version = "1.0.137", default-features = false, features = ["derive"] }
cw-multi-test = { version = "0.13.2", optional = true }
k256 = { version = "0.9.4", optional = true }

View File

@ -0,0 +1,234 @@
use std::{cell::RefCell, collections::BTreeSet, fmt::Debug, rc::Rc};
use anyhow::{anyhow, bail, ensure};
use cosmwasm_std::{to_binary, Addr, Api, Binary, BlockInfo, CustomQuery, Empty, Querier, Storage};
use cw_multi_test::{AppResponse, CosmosRouter, Module};
use k256::ecdsa::{
self,
signature::{Signature as SigT, Signer, Verifier},
SigningKey,
};
use schemars::JsonSchema;
use serde::de::DeserializeOwned;
use crate::{Signature, WormholeQuery};
#[derive(Debug)]
struct Inner {
index: u32,
expiration: u64,
guardians: [SigningKey; 7],
}
#[derive(Clone, Debug)]
pub struct WormholeKeeper(Rc<RefCell<Inner>>);
impl WormholeKeeper {
pub fn new() -> WormholeKeeper {
let guardians = [
SigningKey::from_bytes(&[
93, 217, 189, 224, 168, 81, 157, 93, 238, 38, 143, 8, 182, 94, 69, 77, 232, 199,
238, 206, 15, 135, 221, 58, 43, 74, 0, 129, 54, 198, 62, 226,
])
.unwrap(),
SigningKey::from_bytes(&[
150, 48, 135, 223, 194, 186, 243, 139, 177, 8, 126, 32, 210, 57, 42, 28, 29, 102,
196, 201, 106, 136, 40, 149, 218, 150, 240, 213, 192, 128, 161, 245,
])
.unwrap(),
SigningKey::from_bytes(&[
121, 51, 199, 93, 237, 227, 62, 220, 128, 129, 195, 4, 190, 163, 254, 12, 212, 224,
188, 76, 141, 242, 229, 121, 192, 5, 161, 176, 136, 99, 83, 53,
])
.unwrap(),
SigningKey::from_bytes(&[
224, 180, 4, 114, 215, 161, 184, 12, 218, 96, 20, 141, 154, 242, 46, 230, 167, 165,
54, 141, 108, 64, 146, 27, 193, 89, 251, 139, 234, 132, 124, 30,
])
.unwrap(),
SigningKey::from_bytes(&[
69, 1, 17, 179, 19, 47, 56, 47, 255, 219, 143, 89, 115, 54, 242, 209, 163, 131,
225, 30, 59, 195, 217, 141, 167, 253, 6, 95, 252, 52, 7, 223,
])
.unwrap(),
SigningKey::from_bytes(&[
181, 3, 165, 125, 15, 200, 155, 56, 157, 204, 105, 221, 203, 149, 215, 175, 220,
228, 200, 37, 169, 39, 68, 127, 132, 196, 203, 232, 155, 55, 67, 253,
])
.unwrap(),
SigningKey::from_bytes(&[
72, 81, 175, 107, 23, 108, 178, 66, 32, 53, 14, 117, 233, 33, 114, 102, 68, 89, 83,
201, 129, 57, 56, 130, 214, 212, 172, 16, 23, 22, 234, 160,
])
.unwrap(),
];
WormholeKeeper(Rc::new(RefCell::new(Inner {
index: 0,
expiration: 0,
guardians,
})))
}
pub fn sign(&self, msg: &[u8]) -> Vec<Signature> {
self.0
.borrow()
.guardians
.iter()
.map(|g| {
<SigningKey as Signer<ecdsa::Signature>>::sign(g, msg)
.as_bytes()
.to_vec()
.into()
})
.enumerate()
.map(|(idx, sig)| Signature {
index: idx as u8,
signature: sig,
})
.collect()
}
pub fn verify_quorum(
&self,
data: &[u8],
index: u32,
signatures: &[Signature],
block_time: u64,
) -> anyhow::Result<Empty> {
let mut signers = BTreeSet::new();
for s in signatures {
self.verify_signature(data, index, s, block_time)?;
signers.insert(s.index);
}
if signers.len() as u32 >= self.calculate_quorum(index, block_time)? {
Ok(Empty {})
} else {
Err(anyhow!("no quorum"))
}
}
pub fn verify_signature(
&self,
data: &[u8],
index: u32,
sig: &Signature,
block_time: u64,
) -> anyhow::Result<Empty> {
let this = self.0.borrow();
ensure!(this.index == index, "invalid guardian set");
ensure!(
this.expiration == 0 || block_time < this.expiration,
"guardian set expired"
);
if let Some(g) = this.guardians.get(sig.index as usize) {
let s = ecdsa::Signature::try_from(&*sig.signature).unwrap();
g.verifying_key()
.verify(data, &s)
.map(|()| Empty {})
.map_err(From::from)
} else {
Err(anyhow!("invalid guardian index"))
}
}
pub fn calculate_quorum(&self, index: u32, block_time: u64) -> anyhow::Result<u32> {
let this = self.0.borrow();
ensure!(this.index == index, "invalid guardian set");
ensure!(
this.expiration == 0 || block_time < this.expiration,
"guardian set expired"
);
Ok(((this.guardians.len() as u32 * 10 / 3) * 2) / 10 + 1)
}
pub fn query(&self, request: WormholeQuery, block: &BlockInfo) -> anyhow::Result<Binary> {
match request {
WormholeQuery::VerifyQuorum {
data,
guardian_set_index,
signatures,
} => self
.verify_quorum(&data, guardian_set_index, &signatures, block.height)
.and_then(|e| to_binary(&e).map_err(From::from)),
WormholeQuery::VerifySignature {
data,
guardian_set_index,
signature,
} => self
.verify_signature(&data, guardian_set_index, &signature, block.height)
.and_then(|e| to_binary(&e).map_err(From::from)),
WormholeQuery::CalculateQuorum { guardian_set_index } => self
.calculate_quorum(guardian_set_index, block.height)
.and_then(|q| to_binary(&q).map_err(From::from)),
}
}
pub fn expiration(&self) -> u64 {
self.0.borrow().expiration
}
pub fn set_expiration(&self, expiration: u64) {
self.0.borrow_mut().expiration = expiration;
}
pub fn guardian_set_index(&self) -> u32 {
self.0.borrow().index
}
pub fn set_index(&self, index: u32) {
self.0.borrow_mut().index = index;
}
}
impl Default for WormholeKeeper {
fn default() -> Self {
Self::new()
}
}
impl Module for WormholeKeeper {
type ExecT = Empty;
type QueryT = WormholeQuery;
type SudoT = Empty;
fn execute<ExecC, QueryC>(
&self,
_api: &dyn Api,
_storage: &mut dyn Storage,
_router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
_block: &BlockInfo,
sender: Addr,
msg: Self::ExecT,
) -> anyhow::Result<AppResponse>
where
ExecC: Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static,
QueryC: CustomQuery + DeserializeOwned + 'static,
{
bail!("Unexpected exec msg {msg:?} from {sender}")
}
fn sudo<ExecC, QueryC>(
&self,
_api: &dyn Api,
_storage: &mut dyn Storage,
_router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
_block: &BlockInfo,
msg: Self::SudoT,
) -> anyhow::Result<AppResponse> {
bail!("Unexpected sudo msg {msg:?}")
}
fn query(
&self,
_api: &dyn Api,
_storage: &dyn Storage,
_querier: &dyn Querier,
block: &BlockInfo,
request: Self::QueryT,
) -> anyhow::Result<Binary> {
self.query(request, block)
}
}

View File

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

View File

@ -22,6 +22,7 @@ const artifacts = [
"mock_bridge_integration_2.wasm",
"shutdown_core_bridge_cosmwasm.wasm",
"shutdown_token_bridge_cosmwasm.wasm",
"wormchain_accounting.wasm",
];
/* Check that the artifact folder contains all the wasm files we expect and nothing else */