From 112e6f48a8878f30a7003ea31973bccef52e368e Mon Sep 17 00:00:00 2001 From: The Larry <26318510+0xlarry@users.noreply.github.com> Date: Wed, 2 Feb 2022 19:37:19 +0000 Subject: [PATCH 1/3] Add functions to validate native denoms against a whitelist --- src/asset.rs | 25 ++++++++++++++++++++++--- src/asset_info.rs | 33 ++++++++++++++++++++++++++++++--- src/asset_list.rs | 17 +++++++++++++++-- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/src/asset.rs b/src/asset.rs index 50e78f4..1f0ea35 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -117,6 +117,15 @@ impl AssetUnchecked { amount: self.amount, }) } + + /// Similar to `check`, but in case `self` is a native token, also verifies its denom is included + /// in a given whitelist + pub fn check_whitelist(&self, api: &dyn Api, whitelist: &[&str]) -> StdResult { + Ok(Asset { + info: self.info.check_whitelist(api, whitelist)?, + amount: self.amount, + }) + } } impl fmt::Display for Asset { @@ -307,6 +316,7 @@ impl std::cmp::PartialEq for Asset { #[cfg(test)] mod tests { use super::*; + use crate::AssetInfoUnchecked; use cosmwasm_std::testing::MockApi; #[derive(Serialize)] @@ -367,13 +377,22 @@ mod tests { } #[test] - fn casting() { + fn checking() { let api = MockApi::default(); - let checked = Asset::cw20(Addr::unchecked("mock_token"), 123456u128); + let checked = Asset::cw20(Addr::unchecked("mock_token"), 12345u128); let unchecked: AssetUnchecked = checked.clone().into(); - assert_eq!(unchecked.check(&api).unwrap(), checked); + + let checked = Asset::native("uusd", 12345u128); + let unchecked: AssetUnchecked = checked.clone().into(); + assert_eq!(unchecked.check_whitelist(&api, &["uusd", "uluna", "uosmo"]).unwrap(), checked); + + let unchecked = AssetUnchecked::new(AssetInfoUnchecked::native("uatom"), 12345u128); + assert_eq!( + unchecked.check_whitelist(&api, &["uusd", "uluna", "uosmo"]), + Err(StdError::generic_err("invalid denom uatom; must be uusd|uluna|uosmo")), + ); } #[test] diff --git a/src/asset_info.rs b/src/asset_info.rs index ff6bfa2..20c0732 100644 --- a/src/asset_info.rs +++ b/src/asset_info.rs @@ -1,8 +1,8 @@ use std::fmt; use cosmwasm_std::{ - to_binary, Addr, Api, BalanceResponse, BankQuery, QuerierWrapper, QueryRequest, StdResult, - Uint128, WasmQuery, + to_binary, Addr, Api, BalanceResponse, BankQuery, QuerierWrapper, QueryRequest, StdError, + StdResult, Uint128, WasmQuery, }; use cw20::{BalanceResponse as Cw20BalanceResponse, Cw20QueryMsg}; @@ -88,6 +88,24 @@ impl AssetInfoUnchecked { AssetInfoUnchecked::Native(denom) => AssetInfo::Native(denom.clone()), }) } + + /// Similar to `check`, but in case `self` is a native token, also verifies its denom is included + /// in a given whitelist + pub fn check_whitelist(&self, api: &dyn Api, whitelist: &[&str]) -> StdResult { + Ok(match self { + AssetInfoUnchecked::Cw20(contract_addr) => { + AssetInfo::Cw20(api.addr_validate(contract_addr)?) + } + AssetInfoUnchecked::Native(denom) => { + if !whitelist.contains(&&denom[..]) { + return Err(StdError::generic_err( + format!("invalid denom {}; must be {}", denom, whitelist.join("|")) + )); + } + AssetInfo::Native(denom.clone()) + } + }) + } } impl fmt::Display for AssetInfo { @@ -259,8 +277,17 @@ mod test { let checked = AssetInfo::cw20(Addr::unchecked("mock_token")); let unchecked: AssetInfoUnchecked = checked.clone().into(); - assert_eq!(unchecked.check(&api).unwrap(), checked); + + let checked = AssetInfo::native("uusd"); + let unchecked: AssetInfoUnchecked = checked.clone().into(); + assert_eq!(unchecked.check_whitelist(&api, &["uusd", "uluna", "uosmo"]).unwrap(), checked); + + let unchecked = AssetInfoUnchecked::native("uatom"); + assert_eq!( + unchecked.check_whitelist(&api, &["uusd", "uluna", "uosmo"]), + Err(StdError::generic_err("invalid denom uatom; must be uusd|uluna|uosmo")), + ); } #[test] diff --git a/src/asset_list.rs b/src/asset_list.rs index 33ef8ec..1537ca8 100644 --- a/src/asset_list.rs +++ b/src/asset_list.rs @@ -52,6 +52,14 @@ impl AssetListUnchecked { self.0.iter().map(|asset| asset.check(api)).collect::>>()?, )) } + + /// Similar to `check`, but in case `self` is a native token, also verifies its denom is included + /// in a given whitelist + pub fn check_whitelist(&self, api: &dyn Api, whitelist: &[&str]) -> StdResult { + Ok(AssetList::from( + self.0.iter().map(|asset| asset.check_whitelist(api, whitelist)).collect::>>()?, + )) + } } impl fmt::Display for AssetList { @@ -478,13 +486,18 @@ mod tests { } #[test] - fn casting() { + fn checking() { let api = MockApi::default(); let checked = mock_list(); let unchecked: AssetListUnchecked = checked.clone().into(); + assert_eq!(unchecked.check(&api).unwrap(), checked.clone()); + assert_eq!(unchecked.check_whitelist(&api, &["uusd", "uluna"]).unwrap(), checked); - assert_eq!(unchecked.check(&api).unwrap(), checked); + assert_eq!( + unchecked.check_whitelist(&api, &["uatom", "uosmo", "uscrt"]), + Err(StdError::generic_err("invalid denom uusd; must be uatom|uosmo|uscrt")), + ); } #[test] From 649bfb7144bda98279f9706c0a189b5b9f27543f Mon Sep 17 00:00:00 2001 From: The Larry <26318510+0xlarry@users.noreply.github.com> Date: Wed, 2 Feb 2022 19:38:08 +0000 Subject: [PATCH 2/3] Remove an unused constant --- src/asset.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/asset.rs b/src/asset.rs index 1f0ea35..fd6bc38 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -10,9 +10,6 @@ use serde::{Deserialize, Serialize}; use super::asset_info::{AssetInfo, AssetInfoBase}; -#[cfg(feature = "terra")] -static DECIMAL_FRACTION: Uint128 = Uint128::new(1_000_000_000_000_000_000u128); - /// Represents a fungible asset with a known amount /// /// Each asset instance contains two values: [`info`], which specifies the asset's type (CW20 or From 9beba1158f8b3e7f06a237c7d35fc89fb1ba3e6b Mon Sep 17 00:00:00 2001 From: The Larry <26318510+0xlarry@users.noreply.github.com> Date: Mon, 7 Feb 2022 21:41:33 +0000 Subject: [PATCH 3/3] Merge `check_whitelist` and `check` into one function --- src/asset.rs | 26 ++++++++++---------------- src/asset_info.rs | 38 ++++++++++++++++---------------------- src/asset_list.rs | 29 +++++++++++++---------------- src/lib.rs | 10 ++++++---- 4 files changed, 45 insertions(+), 58 deletions(-) diff --git a/src/asset.rs b/src/asset.rs index fd6bc38..ffcded3 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -95,31 +95,25 @@ impl From for AssetUnchecked { impl AssetUnchecked { /// Validate data contained in an _unchecked_ **asset** instnace, return a new _checked_ - /// **asset** instance + /// **asset** instance: + /// * For CW20 tokens, assert the contract address is valid + /// * For SDK coins, assert that the denom is included in a given whitelist; skip if the + /// whitelist is not provided /// /// ```rust /// use cosmwasm_std::{Addr, Api}; /// use cw_asset::{Asset, AssetUnchecked}; /// /// fn validate_asset(api: &dyn Api, asset_unchecked: &AssetUnchecked) { - /// match asset_unchecked.check(api) { + /// match asset_unchecked.check(api, Some(&["uatom", "uluna"])) { /// Ok(asset) => println!("asset is valid: {}", asset.to_string()), /// Err(err) => println!("asset is invalid! reason: {}", err) /// } /// } /// ``` - pub fn check(&self, api: &dyn Api) -> StdResult { + pub fn check(&self, api: &dyn Api, optional_whitelist: Option<&[&str]>) -> StdResult { Ok(Asset { - info: self.info.check(api)?, - amount: self.amount, - }) - } - - /// Similar to `check`, but in case `self` is a native token, also verifies its denom is included - /// in a given whitelist - pub fn check_whitelist(&self, api: &dyn Api, whitelist: &[&str]) -> StdResult { - Ok(Asset { - info: self.info.check_whitelist(api, whitelist)?, + info: self.info.check(api, optional_whitelist)?, amount: self.amount, }) } @@ -379,15 +373,15 @@ mod tests { let checked = Asset::cw20(Addr::unchecked("mock_token"), 12345u128); let unchecked: AssetUnchecked = checked.clone().into(); - assert_eq!(unchecked.check(&api).unwrap(), checked); + assert_eq!(unchecked.check(&api, None).unwrap(), checked); let checked = Asset::native("uusd", 12345u128); let unchecked: AssetUnchecked = checked.clone().into(); - assert_eq!(unchecked.check_whitelist(&api, &["uusd", "uluna", "uosmo"]).unwrap(), checked); + assert_eq!(unchecked.check(&api, Some(&["uusd", "uluna", "uosmo"])).unwrap(), checked); let unchecked = AssetUnchecked::new(AssetInfoUnchecked::native("uatom"), 12345u128); assert_eq!( - unchecked.check_whitelist(&api, &["uusd", "uluna", "uosmo"]), + unchecked.check(&api, Some(&["uusd", "uluna", "uosmo"])), Err(StdError::generic_err("invalid denom uatom; must be uusd|uluna|uosmo")), ); } diff --git a/src/asset_info.rs b/src/asset_info.rs index 20c0732..238fdea 100644 --- a/src/asset_info.rs +++ b/src/asset_info.rs @@ -67,40 +67,34 @@ impl From for AssetInfoUnchecked { impl AssetInfoUnchecked { /// Validate data contained in an _unchecked_ **asset info** instance; return a new _checked_ - /// **asset info** instance - /// + /// **asset info** instance: + /// * For CW20 tokens, assert the contract address is valid + /// * For SDK coins, assert that the denom is included in a given whitelist; skip if the + /// whitelist is not provided + /// /// ```rust /// use cosmwasm_std::{Addr, Api, StdResult}; /// use cw_asset::{AssetInfo, AssetInfoUnchecked}; /// /// fn validate_asset_info(api: &dyn Api, info_unchecked: &AssetInfoUnchecked) { - /// match info_unchecked.check(api) { + /// match info_unchecked.check(api, Some(&["uatom", "uluna"])) { /// Ok(info) => println!("asset info is valid: {}", info.to_string()), /// Err(err) => println!("asset is invalid! reason: {}", err), /// } /// } /// ``` - pub fn check(&self, api: &dyn Api) -> StdResult { - Ok(match self { - AssetInfoUnchecked::Cw20(contract_addr) => { - AssetInfo::Cw20(api.addr_validate(contract_addr)?) - } - AssetInfoUnchecked::Native(denom) => AssetInfo::Native(denom.clone()), - }) - } - - /// Similar to `check`, but in case `self` is a native token, also verifies its denom is included - /// in a given whitelist - pub fn check_whitelist(&self, api: &dyn Api, whitelist: &[&str]) -> StdResult { + pub fn check(&self, api: &dyn Api, optional_whitelist: Option<&[&str]>) -> StdResult { Ok(match self { AssetInfoUnchecked::Cw20(contract_addr) => { AssetInfo::Cw20(api.addr_validate(contract_addr)?) } AssetInfoUnchecked::Native(denom) => { - if !whitelist.contains(&&denom[..]) { - return Err(StdError::generic_err( - format!("invalid denom {}; must be {}", denom, whitelist.join("|")) - )); + if let Some(whitelist) = optional_whitelist { + if !whitelist.contains(&&denom[..]) { + return Err(StdError::generic_err( + format!("invalid denom {}; must be {}", denom, whitelist.join("|")) + )); + } } AssetInfo::Native(denom.clone()) } @@ -277,15 +271,15 @@ mod test { let checked = AssetInfo::cw20(Addr::unchecked("mock_token")); let unchecked: AssetInfoUnchecked = checked.clone().into(); - assert_eq!(unchecked.check(&api).unwrap(), checked); + assert_eq!(unchecked.check(&api, None).unwrap(), checked); let checked = AssetInfo::native("uusd"); let unchecked: AssetInfoUnchecked = checked.clone().into(); - assert_eq!(unchecked.check_whitelist(&api, &["uusd", "uluna", "uosmo"]).unwrap(), checked); + assert_eq!(unchecked.check(&api, Some(&["uusd", "uluna", "uosmo"])).unwrap(), checked); let unchecked = AssetInfoUnchecked::native("uatom"); assert_eq!( - unchecked.check_whitelist(&api, &["uusd", "uluna", "uosmo"]), + unchecked.check(&api, Some(&["uusd", "uluna", "uosmo"])), Err(StdError::generic_err("invalid denom uatom; must be uusd|uluna|uosmo")), ); } diff --git a/src/asset_list.rs b/src/asset_list.rs index 1537ca8..d004310 100644 --- a/src/asset_list.rs +++ b/src/asset_list.rs @@ -34,30 +34,28 @@ impl From for AssetListUnchecked { impl AssetListUnchecked { /// Validate data contained in an _unchecked_ **asset list** instance, return a new _checked_ - /// **asset list** instance + /// **asset list** instance: + /// * For CW20 tokens, assert the contract address is valid + /// * For SDK coins, assert that the denom is included in a given whitelist; skip if the + /// whitelist is not provided /// /// ```rust /// use cosmwasm_std::{Addr, Api, StdResult}; /// use cw_asset::{Asset, AssetList, AssetUnchecked, AssetListUnchecked}; /// /// fn validate_assets(api: &dyn Api, list_unchecked: &AssetListUnchecked) { - /// match list_unchecked.check(api) { + /// match list_unchecked.check(api, Some(&["uatom", "uluna"])) { /// Ok(list) => println!("asset list is valid: {}", list.to_string()), /// Err(err) => println!("asset list is invalid! reason: {}", err), /// } /// } /// ``` - pub fn check(&self, api: &dyn Api) -> StdResult { + pub fn check(&self, api: &dyn Api, optional_whitelist: Option<&[&str]>) -> StdResult { Ok(AssetList::from( - self.0.iter().map(|asset| asset.check(api)).collect::>>()?, - )) - } - - /// Similar to `check`, but in case `self` is a native token, also verifies its denom is included - /// in a given whitelist - pub fn check_whitelist(&self, api: &dyn Api, whitelist: &[&str]) -> StdResult { - Ok(AssetList::from( - self.0.iter().map(|asset| asset.check_whitelist(api, whitelist)).collect::>>()?, + self.0 + .iter() + .map(|asset| asset.check(api, optional_whitelist)) + .collect::>>()? )) } } @@ -491,11 +489,10 @@ mod tests { let checked = mock_list(); let unchecked: AssetListUnchecked = checked.clone().into(); - assert_eq!(unchecked.check(&api).unwrap(), checked.clone()); - assert_eq!(unchecked.check_whitelist(&api, &["uusd", "uluna"]).unwrap(), checked); - + assert_eq!(unchecked.check(&api, None).unwrap(), checked.clone()); + assert_eq!(unchecked.check(&api, Some(&["uusd", "uluna"])).unwrap(), checked); assert_eq!( - unchecked.check_whitelist(&api, &["uatom", "uosmo", "uscrt"]), + unchecked.check(&api, Some(&["uatom", "uosmo", "uscrt"])), Err(StdError::generic_err("invalid denom uusd; must be uatom|uosmo|uscrt")), ); } diff --git a/src/lib.rs b/src/lib.rs index f1846e9..296b13a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,9 +49,9 @@ //! //! ## Use in messages //! -//! [`Asset`] and [`AssetList`] each come with an _unchecked_ counterpart which contains unverified -//! addresses and implements traits that allow them to be serialized into JSON, so that they can be -//! directly used in Cosmos messages: +//! [`Asset`] and [`AssetList`] each comes with an _unchecked_ counterpart which contains unverified +//! addresses and/or denoms, and implements traits that allow them to be serialized into JSON, so +//! that they can be directly used in Cosmos messages: //! //! ```rust //! use serde::{Serialize, Deserialize}; @@ -74,9 +74,11 @@ //! ```rust //! use cosmwasm_std::{Api, StdResult}; //! use cw_asset::{Asset, AssetUnchecked}; +//! +//! const ACCEPTED_DENOMS: &[&str] = &["uatom", "uosmo", "uluna"]; //! //! fn validate_deposit(api: &dyn Api, asset_unchecked: AssetUnchecked) -> StdResult<()> { -//! let asset: Asset = asset_unchecked.check(api)?; +//! let asset: Asset = asset_unchecked.check(api, Some(ACCEPTED_DENOMS))?; //! Ok(()) //! } //! ```