diff --git a/cosmwasm/Cargo.lock b/cosmwasm/Cargo.lock index 1b1e1f9e8..fe4297a57 100644 --- a/cosmwasm/Cargo.lock +++ b/cosmwasm/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "accounting" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "hex", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "addr2line" version = "0.17.0" @@ -33,6 +48,9 @@ name = "anyhow" version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" +dependencies = [ + "backtrace", +] [[package]] name = "autocfg" diff --git a/cosmwasm/Cargo.toml b/cosmwasm/Cargo.toml index 284a4613a..c82bede3b 100644 --- a/cosmwasm/Cargo.toml +++ b/cosmwasm/Cargo.toml @@ -6,6 +6,7 @@ members = [ "contracts/token-bridge", "contracts/shutdown-token-bridge", "contracts/mock-bridge-integration", + "packages/accounting", "packages/wormhole-bindings", ] @@ -21,8 +22,8 @@ incremental = false overflow-checks = true [patch.crates-io] +accounting = { path = "packages/accounting" } cw20-wrapped-2 = { path = "contracts/cw20-wrapped" } token-bridge-terra-2 = { path = "contracts/token-bridge" } wormhole-bindings = { path = "packages/wormhole-bindings" } wormhole-bridge-terra-2 = { path = "contracts/wormhole" } - diff --git a/cosmwasm/packages/accounting/Cargo.toml b/cosmwasm/packages/accounting/Cargo.toml new file mode 100644 index 000000000..fbe5481d6 --- /dev/null +++ b/cosmwasm/packages/accounting/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "accounting" +version = "0.1.0" +authors = ["Wormhole Project Contributors"] +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +anyhow = "1" +base64 = "0.13" +cosmwasm-schema = "1" +cosmwasm-std = "1" +cw-storage-plus = "0.13.2" +hex = "0.4.3" +schemars = "0.8.8" +serde = { version = "1.0.137", default-features = false } +thiserror = "1" + +[dev-dependencies] +anyhow = { version = "1", features = ["backtrace"] } diff --git a/cosmwasm/packages/accounting/src/contract.rs b/cosmwasm/packages/accounting/src/contract.rs new file mode 100644 index 000000000..c0940c143 --- /dev/null +++ b/cosmwasm/packages/accounting/src/contract.rs @@ -0,0 +1,1161 @@ +use std::marker::PhantomData; + +use anyhow::{bail, ensure, Context}; +use cosmwasm_std::{CustomQuery, Deps, DepsMut, Event, Order, StdResult}; +use cw_storage_plus::Bound; +use thiserror::Error as ThisError; + +use crate::{ + msg::Instantiate, + state::{ + account::{self, Balance}, + transfer, Account, Kind, Modification, Transfer, ACCOUNTS, MODIFICATIONS, TRANSFERS, + }, +}; + +/// Instantiate the on-chain state for accounting. Unlike other methods in this crate, +/// `instantiate` does not perform any validation of the data in `init`. +pub fn instantiate(deps: DepsMut, init: Instantiate) -> anyhow::Result { + let num_accounts = init.accounts.len(); + let num_transfers = init.transfers.len(); + let num_modifications = init.modifications.len(); + + for a in init.accounts { + ACCOUNTS + .save(deps.storage, a.key, &a.balance) + .context("failed to save `Account`")?; + } + + for t in init.transfers { + TRANSFERS + .save(deps.storage, t.key, &t.data) + .context("failed to save `Transfer`")?; + } + + for m in init.modifications { + MODIFICATIONS + .save(deps.storage, m.sequence, &m) + .context("failed to save `Modification`")?; + } + + Ok(Event::new("InstantiateAccounting") + .add_attribute("num_accounts", num_accounts.to_string()) + .add_attribute("num_transfers", num_transfers.to_string()) + .add_attribute("num_modifications", num_modifications.to_string())) +} + +#[derive(ThisError, Debug)] +pub enum CommitTransferError { + #[error("transfer already committed")] + DuplicateTransfer, + #[error("insufficient balance in destination account")] + InsufficientDestinationBalance, + #[error("insufficient balance in source account")] + InsufficientSourceBalance, + #[error("cannot unlock native tokens without an existing native account")] + MissingNativeAccount, + #[error("cannot burn wrapped tokens without an existing wrapped account")] + MissingWrappedAccount, +} + +/// Commits a transfer to the on-chain state. If an error occurs that is not due to the underlying +/// cosmwasm framework, the returned error will be downcastable to `CommitTransferError`. +/// +/// # Examples +/// +/// ``` +/// # fn example() -> anyhow::Result<()> { +/// # use accounting::{ +/// # commit_transfer, +/// # state::{transfer, Transfer}, +/// # CommitTransferError, +/// # }; +/// # use cosmwasm_std::{testing::mock_dependencies, Uint256}; +/// # +/// # let mut deps = mock_dependencies(); +/// # let tx = Transfer { +/// # key: transfer::Key::new(3, [1u8; 32].into(), 5), +/// # data: transfer::Data { +/// # amount: Uint256::from(400u128), +/// # token_chain: 3, +/// # token_address: [3u8; 32].into(), +/// # recipient_chain: 9, +/// # }, +/// # }; +/// # +/// commit_transfer(deps.as_mut(), tx.clone())?; +/// +/// // Repeating the transfer should return an error. +/// let err = commit_transfer(deps.as_mut(), tx) +/// .expect_err("successfully committed duplicate transfer"); +/// if let Some(e) = err.downcast_ref::() { +/// assert!(matches!(e, CommitTransferError::DuplicateTransfer)); +/// } else { +/// println!("framework error: {err:#}"); +/// } +/// # +/// # Ok(()) +/// # } +/// # +/// # example().unwrap(); +/// ``` +pub fn commit_transfer(deps: DepsMut, t: Transfer) -> anyhow::Result { + if TRANSFERS.has(deps.storage, t.key.clone()) { + bail!(CommitTransferError::DuplicateTransfer); + } + + let mut src = { + let key = account::Key::new( + t.key.emitter_chain(), + t.data.token_chain, + t.data.token_address, + ); + let balance = match ACCOUNTS + .may_load(deps.storage, key.clone()) + .context("failed to load source account")? + { + Some(s) => s, + None => { + ensure!( + key.chain_id() == key.token_chain(), + CommitTransferError::MissingWrappedAccount + ); + + Balance::zero() + } + }; + Account { key, balance } + }; + + let mut dst = { + let key = account::Key::new( + t.data.recipient_chain, + t.data.token_chain, + t.data.token_address, + ); + let balance = match ACCOUNTS + .may_load(deps.storage, key.clone()) + .context("failed to load destination account")? + { + Some(b) => b, + None => { + ensure!( + key.chain_id() != key.token_chain(), + CommitTransferError::MissingNativeAccount, + ); + + Balance::zero() + } + }; + Account { key, balance } + }; + + src.lock_or_burn(t.data.amount) + .context(CommitTransferError::InsufficientSourceBalance)?; + dst.unlock_or_mint(t.data.amount) + .context(CommitTransferError::InsufficientDestinationBalance)?; + + ACCOUNTS + .save(deps.storage, src.key, &src.balance) + .context("failed to save updated source account")?; + ACCOUNTS + .save(deps.storage, dst.key, &dst.balance) + .context("failed to save updated destination account")?; + + let evt = Event::new("CommitTransfer") + .add_attribute("key", t.key.to_string()) + .add_attribute("amount", t.data.amount.to_string()) + .add_attribute("token_chain", t.data.token_chain.to_string()) + .add_attribute("token_address", t.data.token_address.to_string()) + .add_attribute("recipient_chain", t.data.recipient_chain.to_string()); + + TRANSFERS + .save(deps.storage, t.key, &t.data) + .context("failed to save `transfer::Data`")?; + + Ok(evt) +} + +#[derive(ThisError, Debug)] +pub enum ModifyBalanceError { + #[error("modification already processed")] + DuplicateModification, + #[error("insufficient balance in account")] + InsufficientBalance, +} + +/// Modifies the on-chain balance of a single account. If an error occurs that is not due to the +/// underlying cosmwasm framework, the returned error will be downcastable to `ModifyBalanceError`. +/// +/// # Examples +/// +/// ``` +/// # fn example() { +/// # use accounting::{ +/// # modify_balance, +/// # state::{Kind, Modification}, +/// # ModifyBalanceError, +/// # }; +/// # use cosmwasm_std::{testing::mock_dependencies, Uint256}; +/// # let mut deps = mock_dependencies(); +/// # +/// /// Subtract the balance from an account that doesn't exist. +/// let m = Modification { +/// sequence: 0, +/// chain_id: 1, +/// token_chain: 2, +/// token_address: [3u8; 32].into(), +/// kind: Kind::Sub, +/// amount: Uint256::from(4u128), +/// reason: "test".into(), +/// }; +/// +/// let err = modify_balance(deps.as_mut(), m) +/// .expect_err("successfully modified account with insufficient balance"); +/// if let Some(e) = err.downcast_ref::() { +/// assert!(matches!(e, ModifyBalanceError::InsufficientBalance)); +/// } else { +/// println!("framework error: {err:#}"); +/// } +/// # } +/// # +/// # example(); +/// ``` +pub fn modify_balance( + deps: DepsMut, + msg: Modification, +) -> anyhow::Result { + if MODIFICATIONS.has(deps.storage, msg.sequence) { + bail!(ModifyBalanceError::DuplicateModification); + } + + let key = ACCOUNTS.key(account::Key::new( + msg.chain_id, + msg.token_chain, + msg.token_address, + )); + + let balance = key + .may_load(deps.storage) + .context("failed to load account")? + .unwrap_or(Balance::zero()); + + let new_balance = match msg.kind { + Kind::Add => balance.checked_add(msg.amount), + Kind::Sub => balance.checked_sub(msg.amount), + } + .map(Balance::from) + .context(ModifyBalanceError::InsufficientBalance)?; + + key.save(deps.storage, &new_balance) + .context("failed to save account")?; + + MODIFICATIONS + .save(deps.storage, msg.sequence, &msg) + .context("failed to store `Modification`")?; + + Ok(Event::new("ModifyBalance") + .add_attribute("sequence", msg.sequence.to_string()) + .add_attribute("chain_id", msg.chain_id.to_string()) + .add_attribute("token_chain", msg.token_chain.to_string()) + .add_attribute("token_address", msg.token_address.to_string()) + .add_attribute("kind", msg.kind.to_string()) + .add_attribute("amount", msg.amount) + .add_attribute("reason", msg.reason)) +} + +/// Query the balance for the account associated with `key`. +pub fn query_balance(deps: Deps, key: account::Key) -> StdResult { + ACCOUNTS.load(deps.storage, key) +} + +/// Query information for all accounts. +pub fn query_all_accounts( + deps: Deps, + start_after: Option, +) -> impl Iterator> + '_ { + let start = start_after.map(|key| Bound::Exclusive((key, PhantomData))); + + ACCOUNTS + .range(deps.storage, start, None, Order::Ascending) + .map(|item| item.map(|(key, balance)| Account { key, balance })) +} + +/// Query the data associated with a transfer. +pub fn query_transfer( + deps: Deps, + key: transfer::Key, +) -> StdResult { + TRANSFERS.load(deps.storage, key) +} + +/// Check if a transfer with the associated key exists. +pub fn has_transfer(deps: Deps, key: transfer::Key) -> bool { + TRANSFERS.has(deps.storage, key) +} + +/// Query information for all transfers. +pub fn query_all_transfers( + deps: Deps, + start_after: Option, +) -> impl Iterator> + '_ { + let start = start_after.map(|key| Bound::Exclusive((key, PhantomData))); + + TRANSFERS + .range(deps.storage, start, None, Order::Ascending) + .map(|item| item.map(|(key, data)| Transfer { key, data })) +} + +/// Query the data associated with a modification. +pub fn query_modification(deps: Deps, sequence: u64) -> StdResult { + MODIFICATIONS.load(deps.storage, sequence) +} + +/// Query information for all modifications. +pub fn query_all_modifications( + deps: Deps, + start_after: Option, +) -> impl Iterator> + '_ { + let start = start_after.map(|seq| Bound::Exclusive((seq, PhantomData))); + + MODIFICATIONS + .range(deps.storage, start, None, Order::Ascending) + .map(|item| item.map(|(_, v)| v)) +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use cosmwasm_std::{testing::mock_dependencies, StdError, Uint256}; + + use super::*; + + fn create_accounts(count: usize) -> Vec { + 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 + } + + fn save_accounts(deps: DepsMut, accounts: &[Account]) { + for a in accounts { + ACCOUNTS + .save(deps.storage, a.key.clone(), &a.balance) + .unwrap(); + } + } + + fn create_transfers(count: usize) -> Vec { + 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 + } + + fn save_transfers(deps: DepsMut, transfers: &[Transfer]) { + for t in transfers { + TRANSFERS + .save(deps.storage, t.key.clone(), &t.data) + .unwrap(); + } + } + + fn create_modifications(count: usize) -> Vec { + 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 + } + + fn save_modifications(deps: DepsMut, modifications: &[Modification]) { + for m in modifications { + MODIFICATIONS.save(deps.storage, m.sequence, m).unwrap(); + } + } + + #[test] + fn instantiate_accounting() { + let mut deps = mock_dependencies(); + let count = 3; + let msg = Instantiate { + accounts: create_accounts(count), + transfers: create_transfers(count), + modifications: create_modifications(count), + }; + + instantiate(deps.as_mut(), msg.clone()).unwrap(); + + for a in msg.accounts { + assert_eq!(a.balance, query_balance(deps.as_ref(), a.key).unwrap()); + } + + for t in msg.transfers { + assert_eq!(t.data, query_transfer(deps.as_ref(), t.key).unwrap()); + } + + for m in msg.modifications { + assert_eq!(m, query_modification(deps.as_ref(), m.sequence).unwrap()); + } + } + + #[test] + fn simple_transfer() { + let mut deps = mock_dependencies(); + let tx = Transfer { + key: transfer::Key::new(3, [1u8; 32].into(), 5), + data: transfer::Data { + amount: Uint256::from(400u128), + token_chain: 3, + token_address: [3u8; 32].into(), + recipient_chain: 9, + }, + }; + + let evt = commit_transfer(deps.as_mut(), tx.clone()).unwrap(); + + let src = account::Key::new( + tx.key.emitter_chain(), + tx.data.token_chain, + tx.data.token_address, + ); + assert_eq!(tx.data.amount, *query_balance(deps.as_ref(), src).unwrap()); + + let dst = account::Key::new( + tx.data.recipient_chain, + tx.data.token_chain, + tx.data.token_address, + ); + assert_eq!(tx.data.amount, *query_balance(deps.as_ref(), dst).unwrap()); + + assert_eq!(evt.ty, "CommitTransfer"); + assert_eq!(evt.attributes.len(), 5); + + let attrs = evt + .attributes + .into_iter() + .map(|attr| (attr.key, attr.value)) + .collect::>(); + assert_eq!(attrs["key"], tx.key.to_string()); + assert_eq!(attrs["amount"], tx.data.amount.to_string()); + assert_eq!(attrs["token_chain"], tx.data.token_chain.to_string()); + assert_eq!(attrs["token_address"], tx.data.token_address.to_string()); + assert_eq!( + attrs["recipient_chain"], + tx.data.recipient_chain.to_string() + ); + + assert_eq!(tx.data, query_transfer(deps.as_ref(), tx.key).unwrap()); + } + + #[test] + fn duplicate_transfer() { + let mut deps = mock_dependencies(); + let tx = Transfer { + key: transfer::Key::new(3, [1u8; 32].into(), 5), + data: transfer::Data { + amount: Uint256::from(400u128), + token_chain: 3, + token_address: [3u8; 32].into(), + recipient_chain: 9, + }, + }; + + commit_transfer(deps.as_mut(), tx.clone()).unwrap(); + + // Repeating the transfer should return an error and should not change the balances. + let e = commit_transfer(deps.as_mut(), tx.clone()) + .expect_err("successfully committed duplicate transfer"); + assert!(matches!( + e.downcast().unwrap(), + CommitTransferError::DuplicateTransfer + )); + + let src = account::Key::new( + tx.key.emitter_chain(), + tx.data.token_chain, + tx.data.token_address, + ); + assert_eq!(tx.data.amount, *query_balance(deps.as_ref(), src).unwrap()); + + let dst = account::Key::new( + tx.data.recipient_chain, + tx.data.token_chain, + tx.data.token_address, + ); + assert_eq!(tx.data.amount, *query_balance(deps.as_ref(), dst).unwrap()); + + assert_eq!(tx.data, query_transfer(deps.as_ref(), tx.key).unwrap()); + } + + #[test] + fn round_trip_transfer() { + let mut deps = mock_dependencies(); + let tx = Transfer { + key: transfer::Key::new(3, [1u8; 32].into(), 5), + data: transfer::Data { + amount: Uint256::from(400u128), + token_chain: 3, + token_address: [3u8; 32].into(), + recipient_chain: 9, + }, + }; + + commit_transfer(deps.as_mut(), tx.clone()).unwrap(); + + let rx = Transfer { + key: transfer::Key::new(tx.data.recipient_chain, [6u8; 32].into(), 2), + data: transfer::Data { + amount: tx.data.amount, + token_chain: tx.data.token_chain, + token_address: tx.data.token_address, + recipient_chain: tx.key.emitter_chain(), + }, + }; + + commit_transfer(deps.as_mut(), rx.clone()).unwrap(); + + let src = account::Key::new( + tx.key.emitter_chain(), + tx.data.token_chain, + tx.data.token_address, + ); + assert_eq!(Balance::zero(), query_balance(deps.as_ref(), src).unwrap()); + + let dst = account::Key::new( + tx.data.recipient_chain, + tx.data.token_chain, + tx.data.token_address, + ); + assert_eq!(Balance::zero(), query_balance(deps.as_ref(), dst).unwrap()); + + assert_eq!(tx.data, query_transfer(deps.as_ref(), tx.key).unwrap()); + assert_eq!(rx.data, query_transfer(deps.as_ref(), rx.key).unwrap()); + } + + #[test] + fn missing_wrapped_account() { + let mut deps = mock_dependencies(); + let tx = Transfer { + key: transfer::Key::new(9, [1u8; 32].into(), 5), + data: transfer::Data { + amount: Uint256::from(400u128), + token_chain: 3, + token_address: [3u8; 32].into(), + recipient_chain: 3, + }, + }; + + let e = commit_transfer(deps.as_mut(), tx) + .expect_err("successfully committed transfer with missing wrapped account"); + assert!(matches!( + e.downcast().unwrap(), + CommitTransferError::MissingWrappedAccount + )); + } + + #[test] + fn missing_native_account() { + let mut deps = mock_dependencies(); + let tx = Transfer { + key: transfer::Key::new(9, [1u8; 32].into(), 5), + data: transfer::Data { + amount: Uint256::from(400u128), + token_chain: 3, + token_address: [3u8; 32].into(), + recipient_chain: 3, + }, + }; + + // Set up a fake wrapped account so the check for the wrapped account succeeds. + ACCOUNTS + .save( + &mut deps.storage, + account::Key::new( + tx.key.emitter_chain(), + tx.data.token_chain, + tx.data.token_address, + ), + &tx.data.amount.into(), + ) + .unwrap(); + + let e = commit_transfer(deps.as_mut(), tx) + .expect_err("successfully committed transfer with missing native account"); + assert!(matches!( + e.downcast().unwrap(), + CommitTransferError::MissingNativeAccount + )); + } + + #[test] + fn repeated_transfer() { + const ITERATIONS: usize = 10; + let mut deps = mock_dependencies(); + let emitter_chain = 3; + let emitter_address = [3u8; 32].into(); + let data = transfer::Data { + amount: Uint256::from(400u128), + token_chain: 3, + token_address: [3u8; 32].into(), + recipient_chain: 9, + }; + + for i in 0..ITERATIONS { + let tx = Transfer { + key: transfer::Key::new(emitter_chain, emitter_address, i as u64), + data: data.clone(), + }; + + commit_transfer(deps.as_mut(), tx).unwrap(); + } + + let src = account::Key::new(emitter_chain, data.token_chain, data.token_address); + assert_eq!( + data.amount * Uint256::from(ITERATIONS as u128), + *query_balance(deps.as_ref(), src).unwrap() + ); + + let dst = account::Key::new(data.recipient_chain, data.token_chain, data.token_address); + assert_eq!( + data.amount * Uint256::from(ITERATIONS as u128), + *query_balance(deps.as_ref(), dst).unwrap() + ); + } + + #[test] + fn wrapped_transfer() { + let mut deps = mock_dependencies(); + + // Do an initial simple transfer. + let tx = Transfer { + key: transfer::Key::new(3, [1u8; 32].into(), 5), + data: transfer::Data { + amount: Uint256::from(400u128), + token_chain: 3, + token_address: [3u8; 32].into(), + recipient_chain: 9, + }, + }; + + commit_transfer(deps.as_mut(), tx.clone()).unwrap(); + + // Now transfer some of the wrapped tokens to a new chain. + let wrapped = Transfer { + key: transfer::Key::new(tx.data.recipient_chain, [2u8; 32].into(), 9), + data: transfer::Data { + amount: Uint256::from(200u128), + token_chain: tx.data.token_chain, + token_address: tx.data.token_address, + recipient_chain: 11, + }, + }; + + commit_transfer(deps.as_mut(), wrapped.clone()).unwrap(); + + // The balance on the original chain should not have changed. + let src = account::Key::new( + tx.key.emitter_chain(), + tx.data.token_chain, + tx.data.token_address, + ); + assert_eq!(tx.data.amount, *query_balance(deps.as_ref(), src).unwrap()); + + // The destination chain should have the difference between the two transfers. + let dst = account::Key::new( + tx.data.recipient_chain, + tx.data.token_chain, + tx.data.token_address, + ); + assert_eq!( + tx.data.amount - wrapped.data.amount, + *query_balance(deps.as_ref(), dst).unwrap() + ); + + // The third chain should only have the wrapped amount. + let w = account::Key::new( + wrapped.data.recipient_chain, + tx.data.token_chain, + tx.data.token_address, + ); + assert_eq!( + wrapped.data.amount, + *query_balance(deps.as_ref(), w).unwrap() + ); + } + + #[test] + fn insufficient_wrapped_balance() { + let mut deps = mock_dependencies(); + let tx = Transfer { + key: transfer::Key::new(3, [1u8; 32].into(), 5), + data: transfer::Data { + amount: Uint256::from(400u128), + token_chain: 3, + token_address: [3u8; 32].into(), + recipient_chain: 9, + }, + }; + + commit_transfer(deps.as_mut(), tx.clone()).unwrap(); + + // Now try to transfer back more tokens than were originally sent. + let rx = Transfer { + key: transfer::Key::new(tx.data.recipient_chain, [6u8; 32].into(), 2), + data: transfer::Data { + amount: tx.data.amount * Uint256::from(2u128), + token_chain: tx.data.token_chain, + token_address: tx.data.token_address, + recipient_chain: tx.key.emitter_chain(), + }, + }; + + let e = commit_transfer(deps.as_mut(), rx) + .expect_err("successfully transferred more tokens than available"); + assert!(matches!( + e.downcast().unwrap(), + CommitTransferError::InsufficientSourceBalance + )); + } + + #[test] + fn insufficient_native_balance() { + let mut deps = mock_dependencies(); + let tx = Transfer { + key: transfer::Key::new(3, [1u8; 32].into(), 5), + data: transfer::Data { + amount: Uint256::from(400u128), + token_chain: 3, + token_address: [3u8; 32].into(), + recipient_chain: 9, + }, + }; + + commit_transfer(deps.as_mut(), tx.clone()).unwrap(); + + // Artificially increase the wrapped balance so that the check for wrapped tokens passes. + ACCOUNTS + .update( + &mut deps.storage, + account::Key::new( + tx.data.recipient_chain, + tx.data.token_chain, + tx.data.token_address, + ), + |b| { + b.unwrap() + .checked_mul(Uint256::from(2u128)) + .map(From::from) + .map_err(|source| StdError::Overflow { source }) + }, + ) + .unwrap(); + + // Now try to transfer back more tokens than were originally sent. + let rx = Transfer { + key: transfer::Key::new(tx.data.recipient_chain, [6u8; 32].into(), 2), + data: transfer::Data { + amount: tx.data.amount * Uint256::from(2u128), + token_chain: tx.data.token_chain, + token_address: tx.data.token_address, + recipient_chain: tx.key.emitter_chain(), + }, + }; + + let e = commit_transfer(deps.as_mut(), rx) + .expect_err("successfully transferred more tokens than available"); + assert!(matches!( + e.downcast().unwrap(), + CommitTransferError::InsufficientDestinationBalance + )); + } + + #[test] + fn simple_modify() { + let mut deps = mock_dependencies(); + let m = Modification { + sequence: 0, + chain_id: 1, + token_chain: 2, + token_address: [3u8; 32].into(), + kind: Kind::Add, + amount: Uint256::from(4u128), + reason: "test".into(), + }; + + let evt = modify_balance(deps.as_mut(), m.clone()).unwrap(); + + let acc = account::Key::new(m.chain_id, m.token_chain, m.token_address); + assert_eq!(m.amount, *query_balance(deps.as_ref(), acc).unwrap()); + + assert_eq!(m, query_modification(deps.as_ref(), m.sequence).unwrap()); + + assert_eq!(evt.ty, "ModifyBalance"); + assert_eq!(evt.attributes.len(), 7); + + let attrs = evt + .attributes + .into_iter() + .map(|attr| (attr.key, attr.value)) + .collect::>(); + assert_eq!(attrs["sequence"], m.sequence.to_string()); + assert_eq!(attrs["chain_id"], m.chain_id.to_string()); + assert_eq!(attrs["token_chain"], m.token_chain.to_string()); + assert_eq!(attrs["token_address"], m.token_address.to_string()); + assert_eq!(attrs["kind"], m.kind.to_string()); + assert_eq!(attrs["amount"], m.amount.to_string()); + assert_eq!(attrs["reason"], m.reason); + } + + #[test] + fn duplicate_modify() { + let mut deps = mock_dependencies(); + let m = Modification { + sequence: 0, + chain_id: 1, + token_chain: 2, + token_address: [3u8; 32].into(), + kind: Kind::Add, + amount: Uint256::from(4u128), + reason: "test".into(), + }; + + modify_balance(deps.as_mut(), m.clone()).unwrap(); + + // Trying the same modification again should fail. + let e = modify_balance(deps.as_mut(), m).expect_err("successfully modified balance twice"); + assert!(matches!( + e.downcast().unwrap(), + ModifyBalanceError::DuplicateModification + )); + } + + #[test] + fn round_trip_modify() { + let mut deps = mock_dependencies(); + let mut m = Modification { + sequence: 0, + chain_id: 1, + token_chain: 2, + token_address: [3u8; 32].into(), + kind: Kind::Add, + amount: Uint256::from(4u128), + reason: "test".into(), + }; + + modify_balance(deps.as_mut(), m.clone()).unwrap(); + + m.sequence += 1; + m.kind = Kind::Sub; + modify_balance(deps.as_mut(), m.clone()).unwrap(); + + let acc = account::Key::new(m.chain_id, m.token_chain, m.token_address); + assert_eq!(Balance::zero(), query_balance(deps.as_ref(), acc).unwrap()); + } + + #[test] + fn repeated_modify() { + const ITERATIONS: u64 = 10; + let mut deps = mock_dependencies(); + let mut m = Modification { + sequence: 0, + chain_id: 1, + token_chain: 2, + token_address: [3u8; 32].into(), + kind: Kind::Add, + amount: Uint256::from(4u128), + reason: "test".into(), + }; + + for i in 0..ITERATIONS { + m.sequence = i; + modify_balance(deps.as_mut(), m.clone()).unwrap(); + } + + let acc = account::Key::new(m.chain_id, m.token_chain, m.token_address); + assert_eq!( + m.amount * Uint256::from(ITERATIONS), + *query_balance(deps.as_ref(), acc).unwrap() + ); + } + + #[test] + fn modify_insufficient_balance() { + let mut deps = mock_dependencies(); + let m = Modification { + sequence: 0, + chain_id: 1, + token_chain: 2, + token_address: [3u8; 32].into(), + kind: Kind::Sub, + amount: Uint256::from(4u128), + reason: "test".into(), + }; + + let e = modify_balance(deps.as_mut(), m) + .expect_err("successfully modified account with insufficient balance"); + assert!(matches!( + e.downcast().unwrap(), + ModifyBalanceError::InsufficientBalance + )); + } + + #[test] + fn query_account_balance() { + let mut deps = mock_dependencies(); + let count = 2; + save_accounts(deps.as_mut(), &create_accounts(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 = query_balance(deps.as_ref(), key).unwrap(); + assert_eq!(balance, Balance::new(Uint256::from(j as u128))) + } + } + } + + #[test] + fn query_missing_account() { + let mut deps = mock_dependencies(); + let count = 2; + save_accounts(deps.as_mut(), &create_accounts(count)); + + let missing = account::Key::new( + (count + 1) as u16, + (count + 2) as u16, + [(count + 3) as u8; 32].into(), + ); + + query_balance(deps.as_ref(), missing) + .expect_err("successfully queried missing account key"); + } + + #[test] + fn query_all_balances() { + let mut deps = mock_dependencies(); + let count = 3; + save_accounts(deps.as_mut(), &create_accounts(count)); + + let found = query_all_accounts(deps.as_ref(), None) + .map(|item| item.map(|acc| (acc.key, acc.balance))) + .collect::>>() + .unwrap(); + 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 query_all_balances_start_after() { + let mut deps = mock_dependencies(); + let count = 3; + save_accounts(deps.as_mut(), &create_accounts(count)); + + for i in 0..count { + for j in 0..count { + let start_after = Some(account::Key::new(i as u16, j as u16, [i as u8; 32].into())); + let found = query_all_accounts(deps.as_ref(), start_after) + .map(|item| item.map(|acc| (acc.key, acc.balance))) + .collect::>>() + .unwrap(); + assert_eq!(found.len(), (count - i - 1) * count + (count - j - 1),); + + for y in j + 1..count { + let key = account::Key::new(i as u16, y as u16, [i as u8; 32].into()); + assert!(found.contains_key(&key)); + } + + for x in i + 1..count { + for y in 0..count { + let key = account::Key::new(x as u16, y as u16, [x as u8; 32].into()); + assert!(found.contains_key(&key)); + } + } + } + } + } + + #[test] + fn query_transfer_data() { + let mut deps = mock_dependencies(); + let count = 2; + save_transfers(deps.as_mut(), &create_transfers(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 = query_transfer(deps.as_ref(), key).unwrap(); + + assert_eq!(expected, actual); + } + } + + #[test] + fn query_missing_transfer() { + let mut deps = mock_dependencies(); + let count = 2; + save_transfers(deps.as_mut(), &create_transfers(count)); + + let missing = transfer::Key::new( + (count + 1) as u16, + [(count + 2) as u8; 32].into(), + (count + 3) as u64, + ); + + query_transfer(deps.as_ref(), missing) + .expect_err("successfully queried missing transfer key"); + } + + #[test] + fn query_all_transfer_data() { + let mut deps = mock_dependencies(); + let count = 3; + save_transfers(deps.as_mut(), &create_transfers(count)); + + let found = query_all_transfers(deps.as_ref(), None) + .map(|item| item.map(|acc| (acc.key, acc.data))) + .collect::>>() + .unwrap(); + 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 query_all_transfer_data_start_after() { + let mut deps = mock_dependencies(); + let count = 3; + save_transfers(deps.as_mut(), &create_transfers(count)); + + for i in 0..count { + let start_after = Some(transfer::Key::new(i as u16, [i as u8; 32].into(), i as u64)); + let found = query_all_transfers(deps.as_ref(), start_after) + .map(|item| item.map(|acc| (acc.key, acc.data))) + .collect::>>() + .unwrap(); + assert_eq!(found.len(), count - i - 1); + + for x in i + 1..count { + let key = transfer::Key::new(x as u16, [x as u8; 32].into(), x as u64); + assert!(found.contains_key(&key)); + } + } + } + + #[test] + fn query_modification_data() { + let mut deps = mock_dependencies(); + let count = 2; + save_modifications(deps.as_mut(), &create_modifications(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 = query_modification(deps.as_ref(), key).unwrap(); + + assert_eq!(expected, actual); + } + } + + #[test] + fn query_missing_modification() { + let mut deps = mock_dependencies(); + let count = 2; + save_modifications(deps.as_mut(), &create_modifications(count)); + + let missing = (count + 1) as u64; + + query_modification(deps.as_ref(), missing) + .expect_err("successfully queried missing modification key"); + } + + #[test] + fn query_all_modification_data() { + let mut deps = mock_dependencies(); + let count = 3; + save_modifications(deps.as_mut(), &create_modifications(count)); + + let found = query_all_modifications(deps.as_ref(), None) + .map(|item| item.map(|m| (m.sequence, m))) + .collect::>>() + .unwrap(); + assert_eq!(found.len(), count); + + for i in 0..count { + let key = i as u64; + assert!(found.contains_key(&key)); + } + } + + #[test] + fn query_all_modification_data_start_after() { + let mut deps = mock_dependencies(); + let count = 3; + save_modifications(deps.as_mut(), &create_modifications(count)); + + for i in 0..count { + let start_after = Some(i as u64); + let found = query_all_modifications(deps.as_ref(), start_after) + .map(|item| item.map(|m| (m.sequence, m))) + .collect::>>() + .unwrap(); + assert_eq!(found.len(), count - i - 1); + + for x in i + 1..count { + let key = x as u64; + assert!(found.contains_key(&key)); + } + } + } +} diff --git a/cosmwasm/packages/accounting/src/lib.rs b/cosmwasm/packages/accounting/src/lib.rs new file mode 100644 index 000000000..feb466218 --- /dev/null +++ b/cosmwasm/packages/accounting/src/lib.rs @@ -0,0 +1,5 @@ +mod contract; +pub mod msg; +pub mod state; + +pub use contract::*; diff --git a/cosmwasm/packages/accounting/src/msg.rs b/cosmwasm/packages/accounting/src/msg.rs new file mode 100644 index 000000000..ffeff4ea4 --- /dev/null +++ b/cosmwasm/packages/accounting/src/msg.rs @@ -0,0 +1,10 @@ +use cosmwasm_schema::cw_serde; + +use crate::state::{Account, Modification, Transfer}; + +#[cw_serde] +pub struct Instantiate { + pub accounts: Vec, + pub transfers: Vec, + pub modifications: Vec, +} diff --git a/cosmwasm/packages/accounting/src/state.rs b/cosmwasm/packages/accounting/src/state.rs new file mode 100644 index 000000000..0b7d6d7df --- /dev/null +++ b/cosmwasm/packages/accounting/src/state.rs @@ -0,0 +1,55 @@ +use std::fmt; + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Uint256; +use cw_storage_plus::Map; + +pub mod account; +mod addr; +pub mod transfer; + +pub use account::Account; +pub use addr::TokenAddress; +pub use transfer::Transfer; + +pub const ACCOUNTS: Map = Map::new("accounting/accounts"); + +pub const TRANSFERS: Map = Map::new("accounting/transfers"); + +#[cw_serde] +#[derive(Eq, PartialOrd, Ord)] +pub enum Kind { + Add, + Sub, +} + +impl fmt::Display for Kind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Kind::Add => f.write_str("ADD"), + Kind::Sub => f.write_str("SUB"), + } + } +} + +#[cw_serde] +#[derive(Eq, PartialOrd, Ord)] +pub struct Modification { + // The sequence number of this modification. Each modification must be + // uniquely identifiable just by its sequnce number. + pub sequence: u64, + // The chain id of the account to be modified. + pub chain_id: u16, + // The chain id of the native chain for the token. + pub token_chain: u16, + // The address of the token on its native chain. + pub token_address: TokenAddress, + // The kind of modification to be made. + pub kind: Kind, + // The amount to be modified. + pub amount: Uint256, + // A human-readable reason for the modification. + pub reason: String, +} + +pub const MODIFICATIONS: Map = Map::new("accounting/modifications"); diff --git a/cosmwasm/packages/accounting/src/state/account.rs b/cosmwasm/packages/accounting/src/state/account.rs new file mode 100644 index 000000000..ff3a3d822 --- /dev/null +++ b/cosmwasm/packages/accounting/src/state/account.rs @@ -0,0 +1,324 @@ +use std::ops::{Deref, DerefMut}; + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{StdResult, Uint256}; +use cw_storage_plus::{Key as CwKey, KeyDeserialize, PrimaryKey}; + +use crate::state::TokenAddress; + +#[cw_serde] +pub struct Account { + pub key: Key, + + // The current balance of the account. + pub balance: Balance, +} + +impl Account { + pub fn lock_or_burn(&mut self, amt: Uint256) -> StdResult<()> { + if self.key.chain_id == self.key.token_chain { + self.balance.0 = self.balance.checked_add(amt)?; + } else { + self.balance.0 = self.balance.checked_sub(amt)?; + } + + Ok(()) + } + + pub fn unlock_or_mint(&mut self, amt: Uint256) -> StdResult<()> { + if self.key.chain_id == self.key.token_chain { + self.balance.0 = self.balance.checked_sub(amt)?; + } else { + self.balance.0 = self.balance.checked_add(amt)?; + } + + Ok(()) + } +} + +#[cw_serde] +#[derive(Eq, PartialOrd, Ord)] +pub struct Key { + // The chain id of the chain to which this account belongs. + chain_id: u16, + // The chain id of the native chain for the token associated with this account. + token_chain: u16, + // The address of the token associated with this account on its native chain. + token_address: TokenAddress, +} + +impl Key { + pub fn new(chain_id: u16, token_chain: u16, token_address: TokenAddress) -> Self { + Self { + chain_id, + token_chain, + token_address, + } + } + + pub fn chain_id(&self) -> u16 { + self.chain_id + } + + pub fn token_chain(&self) -> u16 { + self.token_chain + } + + pub fn token_address(&self) -> &TokenAddress { + &self.token_address + } +} + +impl KeyDeserialize for Key { + type Output = Self; + + fn from_vec(v: Vec) -> StdResult { + <(u16, u16, TokenAddress)>::from_vec(v).map(|(chain_id, token_chain, token_address)| Key { + chain_id, + token_chain, + token_address, + }) + } +} + +impl<'a> PrimaryKey<'a> for Key { + type Prefix = (u16, u16); + type SubPrefix = u16; + type Suffix = TokenAddress; + type SuperSuffix = (u16, TokenAddress); + + fn key(&self) -> Vec { + self.chain_id + .key() + .into_iter() + .chain(self.token_chain.key()) + .chain(self.token_address.key()) + .collect() + } +} + +#[cw_serde] +pub struct Balance(Uint256); + +impl Balance { + pub const fn new(v: Uint256) -> Balance { + Balance(v) + } + + pub const fn zero() -> Balance { + Balance(Uint256::zero()) + } +} + +impl Deref for Balance { + type Target = Uint256; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Balance { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl AsRef for Balance { + fn as_ref(&self) -> &Uint256 { + &self.0 + } +} + +impl AsMut for Balance { + fn as_mut(&mut self) -> &mut Uint256 { + &mut self.0 + } +} + +impl From for Balance { + fn from(v: Uint256) -> Self { + Balance(v) + } +} + +impl From for Uint256 { + fn from(b: Balance) -> Self { + b.0 + } +} + +#[cfg(test)] +mod test { + use cosmwasm_std::StdError; + + use super::*; + + #[test] + fn native_lock() { + let mut acc = Account { + key: Key { + chain_id: 0xbae2, + token_chain: 0xbae2, + token_address: TokenAddress::new([ + 0x62, 0x4e, 0x8d, 0xc6, 0xe0, 0xfe, 0x16, 0xe2, 0x59, 0x6e, 0xcf, 0x9f, 0x90, + 0x0e, 0xd9, 0x5f, 0x4e, 0x6d, 0x26, 0xea, 0xf1, 0x9e, 0xe3, 0xe2, 0x88, 0x63, + 0x60, 0xff, 0xc4, 0x1b, 0xfb, 0x61, + ]), + }, + + balance: Balance(500u128.into()), + }; + + acc.lock_or_burn(200u128.into()).unwrap(); + + assert_eq!(acc.balance.0, Uint256::from(700u128)); + } + + #[test] + fn native_lock_overflow() { + let mut acc = Account { + key: Key { + chain_id: 0xbae2, + token_chain: 0xbae2, + token_address: TokenAddress::new([ + 0x62, 0x4e, 0x8d, 0xc6, 0xe0, 0xfe, 0x16, 0xe2, 0x59, 0x6e, 0xcf, 0x9f, 0x90, + 0x0e, 0xd9, 0x5f, 0x4e, 0x6d, 0x26, 0xea, 0xf1, 0x9e, 0xe3, 0xe2, 0x88, 0x63, + 0x60, 0xff, 0xc4, 0x1b, 0xfb, 0x61, + ]), + }, + + balance: Balance(Uint256::MAX), + }; + + let e = acc.lock_or_burn(200u128.into()).unwrap_err(); + + assert!(matches!(e, StdError::Overflow { .. })) + } + + #[test] + fn native_unlock() { + let mut acc = Account { + key: Key { + chain_id: 0xbae2, + token_chain: 0xbae2, + token_address: TokenAddress::new([ + 0x62, 0x4e, 0x8d, 0xc6, 0xe0, 0xfe, 0x16, 0xe2, 0x59, 0x6e, 0xcf, 0x9f, 0x90, + 0x0e, 0xd9, 0x5f, 0x4e, 0x6d, 0x26, 0xea, 0xf1, 0x9e, 0xe3, 0xe2, 0x88, 0x63, + 0x60, 0xff, 0xc4, 0x1b, 0xfb, 0x61, + ]), + }, + + balance: Balance(500u128.into()), + }; + + acc.unlock_or_mint(200u128.into()).unwrap(); + + assert_eq!(acc.balance.0, Uint256::from(300u128)); + } + + #[test] + fn native_unlock_underflow() { + let mut acc = Account { + key: Key { + chain_id: 0xbae2, + token_chain: 0xbae2, + token_address: TokenAddress::new([ + 0x62, 0x4e, 0x8d, 0xc6, 0xe0, 0xfe, 0x16, 0xe2, 0x59, 0x6e, 0xcf, 0x9f, 0x90, + 0x0e, 0xd9, 0x5f, 0x4e, 0x6d, 0x26, 0xea, 0xf1, 0x9e, 0xe3, 0xe2, 0x88, 0x63, + 0x60, 0xff, 0xc4, 0x1b, 0xfb, 0x61, + ]), + }, + + balance: Balance(Uint256::zero()), + }; + + let e = acc.unlock_or_mint(200u128.into()).unwrap_err(); + + assert!(matches!(e, StdError::Overflow { .. })) + } + + #[test] + fn wrapped_burn() { + let mut acc = Account { + key: Key { + chain_id: 0xcae8, + token_chain: 0xbae2, + token_address: TokenAddress::new([ + 0x62, 0x4e, 0x8d, 0xc6, 0xe0, 0xfe, 0x16, 0xe2, 0x59, 0x6e, 0xcf, 0x9f, 0x90, + 0x0e, 0xd9, 0x5f, 0x4e, 0x6d, 0x26, 0xea, 0xf1, 0x9e, 0xe3, 0xe2, 0x88, 0x63, + 0x60, 0xff, 0xc4, 0x1b, 0xfb, 0x61, + ]), + }, + + balance: Balance(500u128.into()), + }; + + acc.lock_or_burn(200u128.into()).unwrap(); + + assert_eq!(acc.balance.0, Uint256::from(300u128)); + } + + #[test] + fn wrapped_burn_underflow() { + let mut acc = Account { + key: Key { + chain_id: 0xcae8, + token_chain: 0xbae2, + token_address: TokenAddress::new([ + 0x62, 0x4e, 0x8d, 0xc6, 0xe0, 0xfe, 0x16, 0xe2, 0x59, 0x6e, 0xcf, 0x9f, 0x90, + 0x0e, 0xd9, 0x5f, 0x4e, 0x6d, 0x26, 0xea, 0xf1, 0x9e, 0xe3, 0xe2, 0x88, 0x63, + 0x60, 0xff, 0xc4, 0x1b, 0xfb, 0x61, + ]), + }, + + balance: Balance(Uint256::zero()), + }; + + let e = acc.lock_or_burn(200u128.into()).unwrap_err(); + + assert!(matches!(e, StdError::Overflow { .. })) + } + + #[test] + fn wrapped_mint() { + let mut acc = Account { + key: Key { + chain_id: 0xcae8, + token_chain: 0xbae2, + token_address: TokenAddress::new([ + 0x62, 0x4e, 0x8d, 0xc6, 0xe0, 0xfe, 0x16, 0xe2, 0x59, 0x6e, 0xcf, 0x9f, 0x90, + 0x0e, 0xd9, 0x5f, 0x4e, 0x6d, 0x26, 0xea, 0xf1, 0x9e, 0xe3, 0xe2, 0x88, 0x63, + 0x60, 0xff, 0xc4, 0x1b, 0xfb, 0x61, + ]), + }, + + balance: Balance(500u128.into()), + }; + + acc.unlock_or_mint(200u128.into()).unwrap(); + + assert_eq!(acc.balance.0, Uint256::from(700u128)); + } + + #[test] + fn wrapped_mint_overflow() { + let mut acc = Account { + key: Key { + chain_id: 0xcae8, + token_chain: 0xbae2, + token_address: TokenAddress::new([ + 0x62, 0x4e, 0x8d, 0xc6, 0xe0, 0xfe, 0x16, 0xe2, 0x59, 0x6e, 0xcf, 0x9f, 0x90, + 0x0e, 0xd9, 0x5f, 0x4e, 0x6d, 0x26, 0xea, 0xf1, 0x9e, 0xe3, 0xe2, 0x88, 0x63, + 0x60, 0xff, 0xc4, 0x1b, 0xfb, 0x61, + ]), + }, + + balance: Balance(Uint256::MAX), + }; + + let e = acc.unlock_or_mint(200u128.into()).unwrap_err(); + + assert!(matches!(e, StdError::Overflow { .. })) + } +} diff --git a/cosmwasm/packages/accounting/src/state/addr.rs b/cosmwasm/packages/accounting/src/state/addr.rs new file mode 100644 index 000000000..c64843691 --- /dev/null +++ b/cosmwasm/packages/accounting/src/state/addr.rs @@ -0,0 +1,165 @@ +use std::{ + fmt, + ops::{Deref, DerefMut}, + str::FromStr, +}; + +use anyhow::{anyhow, Context}; +use cosmwasm_std::{StdError, StdResult}; +use cw_storage_plus::{Key, KeyDeserialize, Prefixer, PrimaryKey}; +use schemars::JsonSchema; +use serde::{de, Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, JsonSchema)] +#[repr(transparent)] +pub struct TokenAddress(#[schemars(with = "String")] [u8; 32]); + +impl TokenAddress { + pub const fn new(addr: [u8; 32]) -> TokenAddress { + TokenAddress(addr) + } +} + +impl From<[u8; 32]> for TokenAddress { + fn from(addr: [u8; 32]) -> Self { + TokenAddress(addr) + } +} + +impl From for [u8; 32] { + fn from(addr: TokenAddress) -> Self { + addr.0 + } +} + +impl TryFrom> for TokenAddress { + type Error = anyhow::Error; + fn try_from(value: Vec) -> Result { + <[u8; 32]>::try_from(value) + .map(Self) + .map_err(|v: Vec| anyhow!("invalid length; want 32, got {}", v.len())) + } +} + +impl Deref for TokenAddress { + type Target = [u8; 32]; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for TokenAddress { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl AsRef<[u8; 32]> for TokenAddress { + fn as_ref(&self) -> &[u8; 32] { + &self.0 + } +} + +impl AsMut<[u8; 32]> for TokenAddress { + fn as_mut(&mut self) -> &mut [u8; 32] { + &mut self.0 + } +} + +impl AsRef<[u8]> for TokenAddress { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl AsMut<[u8]> for TokenAddress { + fn as_mut(&mut self) -> &mut [u8] { + &mut self.0 + } +} + +impl fmt::Display for TokenAddress { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(&hex::encode(self)) + } +} + +impl FromStr for TokenAddress { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + hex::decode(s) + .context("failed to decode hex") + .and_then(Self::try_from) + } +} + +impl Serialize for TokenAddress { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&base64::encode(self.0)) + } +} + +impl<'de> Deserialize<'de> for TokenAddress { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(Base64Visitor) + } +} + +struct Base64Visitor; + +impl<'de> de::Visitor<'de> for Base64Visitor { + type Value = TokenAddress; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("a valid base64 encoded string of a 32-byte array") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + base64::decode(v) + .map_err(|_| E::invalid_value(de::Unexpected::Str(v), &self)) + .and_then(|b| { + b.try_into() + .map_err(|b: Vec| E::invalid_length(b.len(), &self)) + }) + .map(TokenAddress) + } +} + +impl KeyDeserialize for TokenAddress { + type Output = Self; + + fn from_vec(v: Vec) -> StdResult { + v.try_into() + .map(TokenAddress) + .map_err(|v| StdError::InvalidDataSize { + expected: 32, + actual: v.len() as u64, + }) + } +} + +impl<'a> PrimaryKey<'a> for TokenAddress { + type Prefix = (); + type SubPrefix = (); + type Suffix = Self; + type SuperSuffix = Self; + + fn key(&self) -> Vec { + vec![Key::Ref(&**self)] + } +} + +impl<'a> Prefixer<'a> for TokenAddress { + fn prefix(&self) -> Vec { + vec![Key::Ref(&**self)] + } +} diff --git a/cosmwasm/packages/accounting/src/state/transfer.rs b/cosmwasm/packages/accounting/src/state/transfer.rs new file mode 100644 index 000000000..9b7592a64 --- /dev/null +++ b/cosmwasm/packages/accounting/src/state/transfer.rs @@ -0,0 +1,157 @@ +use std::{fmt, str::FromStr}; + +use anyhow::{ensure, Context}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{StdResult, Uint256}; +use cw_storage_plus::{Key as CwKey, KeyDeserialize, PrimaryKey}; + +use crate::state::TokenAddress; + +#[cw_serde] +pub struct Transfer { + pub key: Key, + pub data: Data, +} + +#[cw_serde] +#[derive(Eq, PartialOrd, Ord, Default, Hash)] +pub struct Key { + // The chain id of the chain on which this transfer originated. + emitter_chain: u16, + + // The address on the emitter chain that created this transfer. + emitter_address: TokenAddress, + + // The sequence number of the transfer. + sequence: u64, +} + +impl Key { + pub fn new(emitter_chain: u16, emitter_address: TokenAddress, sequence: u64) -> Self { + Self { + emitter_chain, + emitter_address, + sequence, + } + } + + pub fn emitter_chain(&self) -> u16 { + self.emitter_chain + } + + pub fn emitter_address(&self) -> &TokenAddress { + &self.emitter_address + } + + pub fn sequence(&self) -> u64 { + self.sequence + } +} + +impl fmt::Display for Key { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{:05}/{}/{:016}", + self.emitter_chain, self.emitter_address, self.sequence + ) + } +} + +impl FromStr for Key { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut components = s.split('/'); + let emitter_chain = components + .next() + .map(str::parse) + .transpose() + .context("failed to parse emitter chain")? + .context("emitter chain missing")?; + let emitter_address = components + .next() + .map(str::parse) + .transpose() + .context("failed to parse emitter address")? + .context("emitter address missing")?; + let sequence = components + .next() + .map(str::parse) + .transpose() + .context("failed to parse sequence")? + .context("sequence missing")?; + + ensure!( + components.next().is_none(), + "unexpected trailing input data" + ); + + Ok(Key { + emitter_chain, + emitter_address, + sequence, + }) + } +} + +impl KeyDeserialize for Key { + type Output = Self; + + fn from_vec(v: Vec) -> StdResult { + <(u16, TokenAddress, u64)>::from_vec(v).map(|(emitter_chain, emitter_address, sequence)| { + Key { + emitter_chain, + emitter_address, + sequence, + } + }) + } +} + +impl<'a> PrimaryKey<'a> for Key { + type Prefix = (u16, TokenAddress); + type SubPrefix = u16; + type Suffix = u64; + type SuperSuffix = (TokenAddress, u64); + + fn key(&self) -> Vec { + self.emitter_chain + .key() + .into_iter() + .chain(self.emitter_address.key()) + .chain(self.sequence.key()) + .collect() + } +} + +#[cw_serde] +pub struct Data { + // The amount to be transferred. + pub amount: Uint256, + + // The id of the native chain of the token. + pub token_chain: u16, + + // The address of the token on its native chain. + pub token_address: TokenAddress, + + // The chain id where the tokens are being sent. + pub recipient_chain: u16, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn key_display() { + let addr = TokenAddress::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 225, 139, 34, 20, 175, 249, 112, 0, 217, 116, + 207, 100, 126, 124, 52, 126, 143, 165, 133, + ]); + let k = Key::new(2, addr, 254278); + let expected = "00002/0000000000000000000000003ee18b2214aff97000d974cf647e7c347e8fa585/0000000000254278"; + assert_eq!(expected, k.to_string()); + } +}