From 3c8eba043d3208a3f973bb81675e535cefd14fc4 Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Wed, 25 Sep 2024 11:12:32 +0700 Subject: [PATCH 01/26] expand scope for setting limiter --- contracts/transmuter/src/contract.rs | 220 ++++++--- contracts/transmuter/src/error.rs | 30 +- contracts/transmuter/src/lib.rs | 1 + contracts/transmuter/src/limiter.rs | 459 +++++++++--------- contracts/transmuter/src/scope.rs | 57 +++ contracts/transmuter/src/swap.rs | 40 +- .../transmuter/src/test/cases/scenarios.rs | 42 +- 7 files changed, 523 insertions(+), 326 deletions(-) create mode 100644 contracts/transmuter/src/scope.rs diff --git a/contracts/transmuter/src/contract.rs b/contracts/transmuter/src/contract.rs index 9e7df39..0828a67 100644 --- a/contracts/transmuter/src/contract.rs +++ b/contracts/transmuter/src/contract.rs @@ -1,3 +1,4 @@ +use crate::scope::Scope; use std::{collections::BTreeMap, iter}; use crate::{ @@ -17,7 +18,7 @@ use cosmwasm_std::{ SubMsg, Uint128, }; -use cw_storage_plus::Item; +use cw_storage_plus::{Item, Map}; use osmosis_std::types::{ cosmos::bank::v1beta1::Metadata, osmosis::tokenfactory::v1beta1::{MsgCreateDenom, MsgCreateDenomResponse, MsgSetDenomMetadata}, @@ -37,12 +38,16 @@ const CREATE_ALLOYED_DENOM_REPLY_ID: u64 = 1; /// Prefix for alloyed asset denom const ALLOYED_PREFIX: &str = "alloyed"; +// asset_group: label -> denoms +pub type AssetGroup<'a> = Map<'a, &'a str, Vec>; + pub struct Transmuter<'a> { pub(crate) active_status: Item<'a, bool>, pub(crate) pool: Item<'a, TransmuterPool>, pub(crate) alloyed_asset: AlloyedAsset<'a>, pub(crate) role: Role<'a>, pub(crate) limiters: Limiters<'a>, + pub(crate) asset_group: AssetGroup<'a>, } pub mod key { @@ -53,6 +58,7 @@ pub mod key { pub const ADMIN: &str = "admin"; pub const MODERATOR: &str = "moderator"; pub const LIMITERS: &str = "limiters"; + pub const ASSET_GROUP: &str = "asset_group"; } #[contract] @@ -69,6 +75,7 @@ impl Transmuter<'_> { ), role: Role::new(key::ADMIN, key::MODERATOR), limiters: Limiters::new(key::LIMITERS), + asset_group: AssetGroup::new(key::ASSET_GROUP), } } @@ -228,12 +235,81 @@ impl Transmuter<'_> { self.limiters.reset_change_limiter_states( deps.storage, env.block.time, - pool.weights()?.unwrap_or_default(), + pool.weights()? + .unwrap_or_default() + .into_iter() + .map(|(denom, weight)| (Scope::denom(&denom).key(), weight)) // TODO: handle asset group + .collect(), )?; Ok(Response::new().add_attribute("method", "add_new_assets")) } + #[sv::msg(exec)] + fn create_asset_group( + &self, + ExecCtx { deps, env: _, info }: ExecCtx, + label: String, + denoms: Vec, + ) -> Result { + nonpayable(&info.funds)?; + + // only admin can create asset group + ensure_admin_authority!(info.sender, self.role.admin, deps.as_ref()); + + // ensure that all denoms are valid pool assets + let pool = self.pool.load(deps.storage)?; + for denom in &denoms { + ensure!( + pool.has_denom(denom), + ContractError::InvalidPoolAssetDenom { + denom: denom.clone() + } + ); + } + + // ensure that asset group does not exist + ensure!( + self.asset_group.may_load(deps.storage, &label)?.is_none(), + ContractError::AssetGroupAlreadyExists { label } + ); + + // save asset group + self.asset_group.save(deps.storage, &label, &denoms)?; + + Ok(Response::new() + .add_attribute("method", "create_asset_group") + .add_attribute("label", label)) + } + + #[sv::msg(exec)] + fn remove_asset_group( + &self, + ExecCtx { deps, env: _, info }: ExecCtx, + label: String, + ) -> Result { + nonpayable(&info.funds)?; + + // only admin can remove asset group + ensure_admin_authority!(info.sender, self.role.admin, deps.as_ref()); + + // check if asset group exists + ensure!( + self.asset_group.load(deps.storage, &label).is_ok(), + ContractError::AssetGroupNotFound { label } + ); + + // remove asset group + self.asset_group.remove(deps.storage, &label); + + // remove all limiter for asset group + todo!("remove all limiter for asset group"); + + // Ok(Response::new() + // .add_attribute("method", "remove_asset_group") + // .add_attribute("label", label)) + } + /// Mark designated denoms as corrupted assets. /// As a result, the corrupted assets will not allowed to be increased by any means, /// both in terms of amount and weight. @@ -285,7 +361,7 @@ impl Transmuter<'_> { fn register_limiter( &self, ExecCtx { deps, env: _, info }: ExecCtx, - denom: String, + scope: Scope, label: String, limiter_params: LimiterParams, ) -> Result { @@ -294,18 +370,30 @@ impl Transmuter<'_> { // only admin can register limiter ensure_admin_authority!(info.sender, self.role.admin, deps.as_ref()); - // ensure pool has the specified denom - let pool = self.pool.load(deps.storage)?; - ensure!( - pool.has_denom(&denom), - ContractError::InvalidPoolAssetDenom { denom } - ); + match scope.clone() { + Scope::Denom(denom) => { + // ensure pool has the specified denom + ensure!( + self.pool.load(deps.storage)?.has_denom(&denom), + ContractError::InvalidPoolAssetDenom { denom } + ); + } + Scope::AssetGroup(label) => { + // check if asset group exists + ensure!( + self.asset_group.may_load(deps.storage, &label)?.is_some(), + ContractError::AssetGroupNotFound { label } + ); + } + }; + let scope_key = scope.key(); let base_attrs = vec![ ("method", "register_limiter"), - ("denom", &denom), ("label", &label), + ("scope", &scope_key), ]; + let limiter_attrs = match &limiter_params { LimiterParams::ChangeLimiter { window_config, @@ -330,7 +418,7 @@ impl Transmuter<'_> { // register limiter self.limiters - .register(deps.storage, &denom, &label, limiter_params)?; + .register(deps.storage, scope, &label, limiter_params)?; Ok(Response::new() .add_attributes(base_attrs) @@ -341,7 +429,7 @@ impl Transmuter<'_> { fn deregister_limiter( &self, ExecCtx { deps, env: _, info }: ExecCtx, - denom: String, + scope: Scope, label: String, ) -> Result { nonpayable(&info.funds)?; @@ -349,14 +437,15 @@ impl Transmuter<'_> { // only admin can deregister limiter ensure_admin_authority!(info.sender, self.role.admin, deps.as_ref()); + let scope_key = scope.key(); let attrs = vec![ ("method", "deregister_limiter"), - ("denom", &denom), + ("scope", &scope_key), ("label", &label), ]; // deregister limiter - self.limiters.deregister(deps.storage, &denom, &label)?; + self.limiters.deregister(deps.storage, scope, &label)?; Ok(Response::new().add_attributes(attrs)) } @@ -365,7 +454,7 @@ impl Transmuter<'_> { fn set_change_limiter_boundary_offset( &self, ExecCtx { deps, env: _, info }: ExecCtx, - denom: String, + scope: Scope, label: String, boundary_offset: Decimal, ) -> Result { @@ -375,9 +464,10 @@ impl Transmuter<'_> { ensure_admin_authority!(info.sender, self.role.admin, deps.as_ref()); let boundary_offset_string = boundary_offset.to_string(); + let scope_key = scope.key(); let attrs = vec![ ("method", "set_change_limiter_boundary_offset"), - ("denom", &denom), + ("scope", &scope_key), ("label", &label), ("boundary_offset", boundary_offset_string.as_str()), ]; @@ -385,7 +475,7 @@ impl Transmuter<'_> { // set boundary offset self.limiters.set_change_limiter_boundary_offset( deps.storage, - &denom, + scope, &label, boundary_offset, )?; @@ -397,7 +487,7 @@ impl Transmuter<'_> { fn set_static_limiter_upper_limit( &self, ExecCtx { deps, env: _, info }: ExecCtx, - denom: String, + scope: Scope, label: String, upper_limit: Decimal, ) -> Result { @@ -407,16 +497,18 @@ impl Transmuter<'_> { ensure_admin_authority!(info.sender, self.role.admin, deps.as_ref()); let upper_limit_string = upper_limit.to_string(); + let scope_key = scope.key(); + let attrs = vec![ ("method", "set_static_limiter_upper_limit"), - ("denom", &denom), + ("scope", &scope_key), ("label", &label), ("upper_limit", upper_limit_string.as_str()), ]; // set upper limit self.limiters - .set_static_limiter_upper_limit(deps.storage, &denom, &label, upper_limit)?; + .set_static_limiter_upper_limit(deps.storage, scope, &label, upper_limit)?; Ok(Response::new().add_attributes(attrs)) } @@ -1019,7 +1111,7 @@ mod tests { let info = mock_info(admin, &[]); for denom in ["uosmo", "uion"] { let register_limiter_msg = ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { - denom: denom.to_string(), + scope: Scope::Denom(denom.to_string()), label: "change_limiter".to_string(), limiter_params: change_limiter_params.clone(), }); @@ -1033,7 +1125,7 @@ mod tests { .unwrap(); let register_limiter_msg = ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { - denom: denom.to_string(), + scope: Scope::Denom(denom.to_string()), label: "static_limiter".to_string(), limiter_params: static_limiter_params.clone(), }); @@ -1067,8 +1159,8 @@ mod tests { execute(deps.as_mut(), env.clone(), info.clone(), join_pool_msg).unwrap(); for denom in ["uosmo", "uion"] { - assert_dirty_change_limiters_by_denom!( - denom, + assert_dirty_change_limiters_by_scope!( + &Scope::denom(denom), Transmuter::default().limiters, deps.as_ref().storage ); @@ -1139,8 +1231,8 @@ mod tests { // Reset change limiter states if new assets are added for denom in ["uosmo", "uion"] { - assert_reset_change_limiters_by_denom!( - denom, + assert_reset_change_limiters_by_scope!( + &Scope::denom(denom), reset_at, transmuter, deps.as_ref().storage @@ -1309,7 +1401,7 @@ mod tests { // set limiters for denom in ["wbtc", "tbtc", "nbtc", "stbtc"] { let register_limiter_msg = ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { - denom: denom.to_string(), + scope: Scope::Denom(denom.to_string()), label: "change_limiter".to_string(), limiter_params: change_limiter_params.clone(), }); @@ -1324,7 +1416,7 @@ mod tests { .unwrap(); let register_limiter_msg = ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { - denom: denom.to_string(), + scope: Scope::Denom(denom.to_string()), label: "static_limiter".to_string(), limiter_params: static_limiter_params.clone(), }); @@ -1613,14 +1705,14 @@ mod tests { assert_eq!( Transmuter::default() .limiters - .list_limiters_by_denom(&deps.storage, "wbtc") + .list_limiters_by_scope(&deps.storage, &Scope::denom("wbtc")) .unwrap(), vec![] ); for denom in ["tbtc", "nbtc", "stbtc"] { - assert_reset_change_limiters_by_denom!( - denom, + assert_reset_change_limiters_by_scope!( + &Scope::denom(denom), env.block.time, Transmuter::default(), deps.as_ref().storage @@ -1719,7 +1811,7 @@ mod tests { assert_eq!( Transmuter::default() .limiters - .list_limiters_by_denom(&deps.storage, "tbtc") + .list_limiters_by_scope(&deps.storage, &Scope::denom("tbtc")) .unwrap() .len(), 2 @@ -2206,7 +2298,7 @@ mod tests { mock_env(), mock_info(user, &[]), ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { - denom: "uosmo".to_string(), + scope: Scope::Denom("uosmo".to_string()), label: "1h".to_string(), limiter_params: LimiterParams::ChangeLimiter { window_config: WindowConfig { @@ -2226,7 +2318,7 @@ mod tests { mock_env(), mock_info(user, &[]), ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { - denom: "uosmo".to_string(), + scope: Scope::Denom("uosmo".to_string()), label: "1h".to_string(), limiter_params: LimiterParams::StaticLimiter { upper_limit: Decimal::percent(60), @@ -2247,7 +2339,7 @@ mod tests { mock_env(), mock_info(admin, &[]), ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { - denom: "uosmo".to_string(), + scope: Scope::Denom("uosmo".to_string()), label: "1h".to_string(), limiter_params: LimiterParams::ChangeLimiter { window_config: window_config_1h.clone(), @@ -2259,8 +2351,8 @@ mod tests { let attrs = vec![ attr("method", "register_limiter"), - attr("denom", "uosmo"), attr("label", "1h"), + attr("scope", "denom::uosmo"), attr("limiter_type", "change_limiter"), attr("window_size", "3600000000000"), attr("division_count", "5"), @@ -2275,7 +2367,7 @@ mod tests { mock_env(), mock_info(admin, &[]), ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { - denom: "invalid_denom".to_string(), + scope: Scope::Denom("invalid_denom".to_string()), label: "1h".to_string(), limiter_params: LimiterParams::ChangeLimiter { window_config: window_config_1h.clone(), @@ -2300,7 +2392,7 @@ mod tests { assert_eq!( limiters.limiters, vec![( - (String::from("uosmo"), String::from("1h")), + (Scope::denom("uosmo").key(), String::from("1h")), Limiter::ChangeLimiter( ChangeLimiter::new(window_config_1h.clone(), Decimal::percent(1)).unwrap() ) @@ -2316,7 +2408,7 @@ mod tests { mock_env(), mock_info(admin, &[]), ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { - denom: "uosmo".to_string(), + scope: Scope::Denom("uosmo".to_string()), label: "1w".to_string(), limiter_params: LimiterParams::ChangeLimiter { window_config: window_config_1w.clone(), @@ -2328,8 +2420,8 @@ mod tests { let attrs_1w = vec![ attr("method", "register_limiter"), - attr("denom", "uosmo"), attr("label", "1w"), + attr("scope", "denom::uosmo"), attr("limiter_type", "change_limiter"), attr("window_size", "604800000000"), attr("division_count", "5"), @@ -2347,13 +2439,13 @@ mod tests { limiters.limiters, vec![ ( - (String::from("uosmo"), String::from("1h")), + (Scope::denom("uosmo").key(), String::from("1h")), Limiter::ChangeLimiter( ChangeLimiter::new(window_config_1h, Decimal::percent(1)).unwrap() ) ), ( - (String::from("uosmo"), String::from("1w")), + (Scope::denom("uosmo").key(), String::from("1w")), Limiter::ChangeLimiter( ChangeLimiter::new(window_config_1w.clone(), Decimal::percent(1)).unwrap() ) @@ -2367,7 +2459,7 @@ mod tests { mock_env(), mock_info(admin, &[]), ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { - denom: "uosmo".to_string(), + scope: Scope::Denom("uosmo".to_string()), label: "static".to_string(), limiter_params: LimiterParams::StaticLimiter { upper_limit: Decimal::percent(60), @@ -2378,8 +2470,8 @@ mod tests { let attrs = vec![ attr("method", "register_limiter"), - attr("denom", "uosmo"), attr("label", "static"), + attr("scope", "denom::uosmo"), attr("limiter_type", "static_limiter"), attr("upper_limit", "0.6"), ]; @@ -2392,7 +2484,7 @@ mod tests { mock_env(), mock_info(user, &[]), ContractExecMsg::Transmuter(ExecMsg::DeregisterLimiter { - denom: "uosmo".to_string(), + scope: Scope::Denom("uosmo".to_string()), label: "1h".to_string(), }), ) @@ -2406,7 +2498,7 @@ mod tests { mock_env(), mock_info(admin, &[]), ContractExecMsg::Transmuter(ExecMsg::DeregisterLimiter { - denom: "uosmo".to_string(), + scope: Scope::Denom("uosmo".to_string()), label: "1h".to_string(), }), ) @@ -2414,7 +2506,7 @@ mod tests { let attrs = vec![ attr("method", "deregister_limiter"), - attr("denom", "uosmo"), + attr("scope", "denom::uosmo"), attr("label", "1h"), ]; @@ -2429,13 +2521,13 @@ mod tests { limiters.limiters, vec![ ( - (String::from("uosmo"), String::from("1w")), + (Scope::denom("uosmo").key(), String::from("1w")), Limiter::ChangeLimiter( ChangeLimiter::new(window_config_1w.clone(), Decimal::percent(1)).unwrap() ) ), ( - (String::from("uosmo"), String::from("static")), + (Scope::denom("uosmo").key(), String::from("static")), Limiter::StaticLimiter(StaticLimiter::new(Decimal::percent(60)).unwrap()) ) ] @@ -2447,7 +2539,7 @@ mod tests { mock_env(), mock_info(user, &[]), ContractExecMsg::Transmuter(ExecMsg::SetChangeLimiterBoundaryOffset { - denom: "uosmo".to_string(), + scope: Scope::Denom("uosmo".to_string()), label: "1w".to_string(), boundary_offset: Decimal::zero(), }), @@ -2462,7 +2554,7 @@ mod tests { mock_env(), mock_info(admin, &[]), ContractExecMsg::Transmuter(ExecMsg::SetChangeLimiterBoundaryOffset { - denom: "uosmo".to_string(), + scope: Scope::Denom("uosmo".to_string()), label: "1h".to_string(), boundary_offset: Decimal::zero(), }), @@ -2472,7 +2564,7 @@ mod tests { assert_eq!( err, ContractError::LimiterDoesNotExist { - denom: "uosmo".to_string(), + scope: Scope::denom("uosmo"), label: "1h".to_string() } ); @@ -2483,7 +2575,7 @@ mod tests { mock_env(), mock_info(admin, &[]), ContractExecMsg::Transmuter(ExecMsg::SetChangeLimiterBoundaryOffset { - denom: "uosmo".to_string(), + scope: Scope::Denom("uosmo".to_string()), label: "1w".to_string(), boundary_offset: Decimal::percent(10), }), @@ -2492,7 +2584,7 @@ mod tests { let attrs = vec![ attr("method", "set_change_limiter_boundary_offset"), - attr("denom", "uosmo"), + attr("scope", "denom::uosmo"), attr("label", "1w"), attr("boundary_offset", "0.1"), ]; @@ -2508,13 +2600,13 @@ mod tests { limiters.limiters, vec![ ( - (String::from("uosmo"), String::from("1w")), + (Scope::denom("uosmo").key(), String::from("1w")), Limiter::ChangeLimiter( ChangeLimiter::new(window_config_1w.clone(), Decimal::percent(10)).unwrap() ) ), ( - (String::from("uosmo"), String::from("static")), + (Scope::denom("uosmo").key(), String::from("static")), Limiter::StaticLimiter(StaticLimiter::new(Decimal::percent(60)).unwrap()) ) ] @@ -2526,7 +2618,7 @@ mod tests { mock_env(), mock_info(user, &[]), ContractExecMsg::Transmuter(ExecMsg::SetStaticLimiterUpperLimit { - denom: "uosmo".to_string(), + scope: Scope::Denom("uosmo".to_string()), label: "static".to_string(), upper_limit: Decimal::percent(50), }), @@ -2541,7 +2633,7 @@ mod tests { mock_env(), mock_info(admin, &[]), ContractExecMsg::Transmuter(ExecMsg::SetStaticLimiterUpperLimit { - denom: "uosmo".to_string(), + scope: Scope::Denom("uosmo".to_string()), label: "1h".to_string(), upper_limit: Decimal::percent(50), }), @@ -2551,7 +2643,7 @@ mod tests { assert_eq!( err, ContractError::LimiterDoesNotExist { - denom: "uosmo".to_string(), + scope: Scope::denom("uosmo"), label: "1h".to_string() } ); @@ -2562,7 +2654,7 @@ mod tests { mock_env(), mock_info(admin, &[]), ContractExecMsg::Transmuter(ExecMsg::SetStaticLimiterUpperLimit { - denom: "uosmo".to_string(), + scope: Scope::denom("uosmo"), label: "1w".to_string(), upper_limit: Decimal::percent(50), }), @@ -2583,7 +2675,7 @@ mod tests { mock_env(), mock_info(admin, &[]), ContractExecMsg::Transmuter(ExecMsg::SetStaticLimiterUpperLimit { - denom: "uosmo".to_string(), + scope: Scope::Denom("uosmo".to_string()), label: "static".to_string(), upper_limit: Decimal::percent(50), }), @@ -2592,7 +2684,7 @@ mod tests { let attrs = vec![ attr("method", "set_static_limiter_upper_limit"), - attr("denom", "uosmo"), + attr("scope", "denom::uosmo"), attr("label", "static"), attr("upper_limit", "0.5"), ]; @@ -2608,13 +2700,13 @@ mod tests { limiters.limiters, vec![ ( - (String::from("uosmo"), String::from("1w")), + (Scope::denom("uosmo").key(), String::from("1w")), Limiter::ChangeLimiter( ChangeLimiter::new(window_config_1w, Decimal::percent(10)).unwrap() ) ), ( - (String::from("uosmo"), String::from("static")), + (Scope::denom("uosmo").key(), String::from("static")), Limiter::StaticLimiter(StaticLimiter::new(Decimal::percent(50)).unwrap()) ) ] diff --git a/contracts/transmuter/src/error.rs b/contracts/transmuter/src/error.rs index 42153c4..d09a055 100644 --- a/contracts/transmuter/src/error.rs +++ b/contracts/transmuter/src/error.rs @@ -4,7 +4,7 @@ use cosmwasm_std::{ }; use thiserror::Error; -use crate::math::MathError; +use crate::{math::MathError, scope::Scope}; #[derive(Error, Debug, PartialEq)] pub enum ContractError { @@ -114,11 +114,11 @@ pub enum ContractError { #[error("Admin transferring state is inoperable for the requested operation")] InoperableAdminTransferringState {}, - #[error("Limiter count for {denom} exceed maximum per denom: {max}")] - MaxLimiterCountPerDenomExceeded { denom: String, max: Uint64 }, + #[error("Limiter count for {scope} exceed maximum per denom: {max}")] + MaxLimiterCountPerDenomExceeded { scope: Scope, max: Uint64 }, - #[error("Denom: {denom} cannot have an empty limiter after it has been registered")] - EmptyLimiterNotAllowed { denom: String }, + #[error("Denom: {scope} cannot have an empty limiter after it has been registered")] + EmptyLimiterNotAllowed { scope: Scope }, #[error("Limiter label must not be empty")] EmptyLimiterLabel {}, @@ -158,17 +158,17 @@ pub enum ContractError { ended_at: Timestamp, }, - #[error("Limiter does not exist for denom: {denom}, label: {label}")] - LimiterDoesNotExist { denom: String, label: String }, + #[error("Limiter does not exist for scope: {scope}, label: {label}")] + LimiterDoesNotExist { scope: Scope, label: String }, - #[error("Limiter already exists for denom: {denom}, label: {label}")] - LimiterAlreadyExists { denom: String, label: String }, + #[error("Limiter already exists for scope: {scope}, label: {label}")] + LimiterAlreadyExists { scope: Scope, label: String }, #[error( - "Upper limit exceeded for `{denom}`, upper limit is {upper_limit}, but the resulted weight is {value}" + "Upper limit exceeded for `{scope}`, upper limit is {upper_limit}, but the resulted weight is {value}" )] UpperLimitExceeded { - denom: String, + scope: Scope, upper_limit: Decimal, value: Decimal, }, @@ -180,7 +180,13 @@ pub enum ContractError { NormalizationFactorMustBePositive {}, #[error("Corrupted asset: {denom} must not increase in amount or weight")] - CorruptedAssetRelativelyIncreased { denom: String }, + CorruptedAssetRelativelyIncreased { denom: String }, // TODO: change this to threshold scope as well + + #[error("Asset group {label} not found")] + AssetGroupNotFound { label: String }, + + #[error("Asset group {label} already exists")] + AssetGroupAlreadyExists { label: String }, #[error("{0}")] OverflowError(#[from] OverflowError), diff --git a/contracts/transmuter/src/lib.rs b/contracts/transmuter/src/lib.rs index 68862f3..6fc9a8c 100644 --- a/contracts/transmuter/src/lib.rs +++ b/contracts/transmuter/src/lib.rs @@ -6,6 +6,7 @@ mod limiter; mod math; mod migrations; mod role; +mod scope; mod sudo; mod swap; mod transmuter_pool; diff --git a/contracts/transmuter/src/limiter.rs b/contracts/transmuter/src/limiter.rs index 1cd1ed8..8271bf8 100644 --- a/contracts/transmuter/src/limiter.rs +++ b/contracts/transmuter/src/limiter.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use crate::ContractError; +use crate::{scope::Scope, ContractError}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ensure, Decimal, StdError, Storage, Timestamp, Uint64}; use cw_storage_plus::Map; @@ -136,7 +136,7 @@ impl ChangeLimiter { fn ensure_upper_limit( self, block_time: Timestamp, - denom: &str, + scope: &Scope, value: Decimal, ) -> Result { let (latest_removed_division, updated_limiter) = @@ -161,7 +161,7 @@ impl ChangeLimiter { ensure!( value <= upper_limit, ContractError::UpperLimitExceeded { - denom: denom.to_string(), + scope: scope.clone(), upper_limit, value, } @@ -285,11 +285,11 @@ impl StaticLimiter { Ok(self) } - fn ensure_upper_limit(self, denom: &str, value: Decimal) -> Result { + fn ensure_upper_limit(self, scope: &Scope, value: Decimal) -> Result { ensure!( value <= self.upper_limit, ContractError::UpperLimitExceeded { - denom: denom.to_string(), + scope: scope.clone(), upper_limit: self.upper_limit, value, } @@ -321,7 +321,7 @@ pub enum LimiterParams { } pub struct Limiters<'a> { - /// Map of (denom, label) -> Limiter + /// Map of (scope, label) -> Limiter limiters: Map<'a, (&'a str, &'a str), Limiter>, } @@ -335,19 +335,21 @@ impl<'a> Limiters<'a> { pub fn register( &self, storage: &mut dyn Storage, - denom: &str, + scope: Scope, label: &str, limiter_params: LimiterParams, ) -> Result<(), ContractError> { - let is_registering_limiter_exists = - self.limiters.may_load(storage, (denom, label))?.is_some(); + let is_registering_limiter_exists = self + .limiters + .may_load(storage, (&scope.key(), label))? + .is_some(); ensure!(!label.is_empty(), ContractError::EmptyLimiterLabel {}); ensure!( !is_registering_limiter_exists, ContractError::LimiterAlreadyExists { - denom: denom.to_string(), + scope, label: label.to_string() } ); @@ -363,31 +365,31 @@ impl<'a> Limiters<'a> { }; // ensure limiters for the denom has not yet reached the maximum - let limiter_count_for_denom = self.list_limiters_by_denom(storage, denom)?.len() as u64; + let limiter_count_for_denom = self.list_limiters_by_scope(storage, &scope)?.len() as u64; ensure!( limiter_count_for_denom < MAX_LIMITER_COUNT_PER_DENOM.u64(), ContractError::MaxLimiterCountPerDenomExceeded { - denom: denom.to_string(), + scope, max: MAX_LIMITER_COUNT_PER_DENOM } ); self.limiters - .save(storage, (denom, label), &limiter) + .save(storage, (&scope.key(), label), &limiter) .map_err(Into::into) } /// Deregsiter all limiters for the denom without checking if it will be empty. /// This is useful when the asset is being removed, so that limiters for the asset are no longer needed. - pub fn uncheck_deregister_all_for_denom( + pub fn uncheck_deregister_all_for_scope( &self, storage: &mut dyn Storage, - denom: &str, + scope: Scope, ) -> Result<(), ContractError> { - let limiters = self.list_limiters_by_denom(storage, denom)?; + let limiters = self.list_limiters_by_scope(storage, &scope)?; for (label, _) in limiters { - self.limiters.remove(storage, (denom, &label)); + self.limiters.remove(storage, (&scope.key(), &label)); } Ok(()) @@ -396,26 +398,25 @@ impl<'a> Limiters<'a> { pub fn deregister( &self, storage: &mut dyn Storage, - denom: &str, + scope: Scope, label: &str, ) -> Result { - match self.limiters.may_load(storage, (denom, label))? { + let scope_key = scope.key(); + match self.limiters.may_load(storage, (&scope_key, label))? { Some(limiter) => { let limiter_for_denom_will_not_be_empty = - self.list_limiters_by_denom(storage, denom)?.len() >= 2; + self.list_limiters_by_scope(storage, &scope)?.len() >= 2; ensure!( limiter_for_denom_will_not_be_empty, - ContractError::EmptyLimiterNotAllowed { - denom: denom.to_string() - } + ContractError::EmptyLimiterNotAllowed { scope } ); - self.limiters.remove(storage, (denom, label)); + self.limiters.remove(storage, (&scope_key, label)); Ok(limiter) } None => Err(ContractError::LimiterDoesNotExist { - denom: denom.to_string(), + scope, label: label.to_string(), }), } @@ -425,16 +426,16 @@ impl<'a> Limiters<'a> { pub fn set_change_limiter_boundary_offset( &self, storage: &mut dyn Storage, - denom: &str, + scope: Scope, label: &str, boundary_offset: Decimal, ) -> Result<(), ContractError> { self.limiters.update( storage, - (denom, label), + (&scope.key(), label), |limiter: Option| -> Result { let limiter = limiter.ok_or(ContractError::LimiterDoesNotExist { - denom: denom.to_string(), + scope, label: label.to_string(), })?; @@ -463,16 +464,16 @@ impl<'a> Limiters<'a> { pub fn set_static_limiter_upper_limit( &self, storage: &mut dyn Storage, - denom: &str, + scope: Scope, label: &str, upper_limit: Decimal, ) -> Result<(), ContractError> { self.limiters.update( storage, - (denom, label), + (&scope.key(), label), |limiter: Option| -> Result { let limiter = limiter.ok_or(ContractError::LimiterDoesNotExist { - denom: denom.to_string(), + scope, label: label.to_string(), })?; @@ -491,14 +492,14 @@ impl<'a> Limiters<'a> { Ok(()) } - pub fn list_limiters_by_denom( + pub fn list_limiters_by_scope( &self, storage: &dyn Storage, - denom: &str, + scope: &Scope, ) -> Result, ContractError> { // there is no need to limit, since the number of limiters is expected to be small self.limiters - .prefix(denom) + .prefix(&scope.key()) .range(storage, None, None, cosmwasm_std::Order::Ascending) .collect::, _>>() .map_err(Into::into) @@ -519,21 +520,21 @@ impl<'a> Limiters<'a> { pub fn check_limits_and_update( &self, storage: &mut dyn Storage, - denom_value_pairs: Vec<(String, (Decimal, Decimal))>, + scope_value_pairs: Vec<(Scope, (Decimal, Decimal))>, block_time: Timestamp, ) -> Result<(), ContractError> { - for (denom, (prev_value, value)) in denom_value_pairs { - let limiters = self.list_limiters_by_denom(storage, denom.as_str())?; + for (scope, (prev_value, value)) in scope_value_pairs { + let limiters = self.list_limiters_by_scope(storage, &scope)?; let is_not_decreasing = value >= prev_value; for (label, limiter) in limiters { // Enforce limiter only if value is increasing, because if the value is decreasing from the previous value, - // for the specific denom, it is a balancing act to move away from the limit. + // for the specific scope, it is a balancing act to move away from the limit. let limiter = match limiter { Limiter::ChangeLimiter(limiter) => Limiter::ChangeLimiter({ if is_not_decreasing { limiter - .ensure_upper_limit(block_time, denom.as_str(), value)? + .ensure_upper_limit(block_time, &scope, value)? .update(block_time, value)? } else { limiter.update(block_time, value)? @@ -541,7 +542,7 @@ impl<'a> Limiters<'a> { }), Limiter::StaticLimiter(limiter) => Limiter::StaticLimiter({ if is_not_decreasing { - limiter.ensure_upper_limit(denom.as_str(), value)? + limiter.ensure_upper_limit(&scope, value)? } else { limiter } @@ -550,7 +551,7 @@ impl<'a> Limiters<'a> { // save updated limiter self.limiters - .save(storage, (denom.as_str(), &label), &limiter)?; + .save(storage, (&scope.key(), &label), &limiter)?; } } @@ -573,13 +574,13 @@ impl<'a> Limiters<'a> { let limiters = self.list_limiters(storage)?; let weights: HashMap = weights.into_iter().collect(); - for ((denom, label), limiter) in limiters { + for ((scope, label), limiter) in limiters { match limiter { Limiter::ChangeLimiter(limiter) => { self.limiters - .save(storage, (denom.as_str(), label.as_str()), { - let value = weights.get(denom.as_str()).copied().ok_or_else(|| { - StdError::not_found(format!("weight for {}", denom)) + .save(storage, (scope.as_str(), label.as_str()), { + let value = weights.get(scope.as_str()).copied().ok_or_else(|| { + StdError::not_found(format!("weight for {}", scope)) })?; &Limiter::ChangeLimiter(limiter.reset().update(block_time, value)?) })? @@ -595,8 +596,8 @@ impl<'a> Limiters<'a> { /// This is used for testing if all change limiters has been newly created or reset. #[cfg(test)] #[macro_export] -macro_rules! assert_reset_change_limiters_by_denom { - ($denom:expr, $reset_at:expr, $transmuter:expr, $storage:expr) => { +macro_rules! assert_reset_change_limiters_by_scope { + ($scope:expr, $reset_at:expr, $transmuter:expr, $storage:expr) => { let pool = $transmuter.pool.load($storage).unwrap(); let weights = pool .weights() @@ -607,12 +608,15 @@ macro_rules! assert_reset_change_limiters_by_denom { let limiters = $transmuter .limiters - .list_limiters_by_denom($storage, $denom) + .list_limiters_by_scope($storage, $scope) .expect("failed to list limiters"); for (_label, limiter) in limiters { if let $crate::limiter::Limiter::ChangeLimiter(limiter) = limiter { - let value = *weights.get($denom).unwrap(); + let value = match $scope { + Scope::Denom(denom) => *weights.get(denom.as_str()).unwrap(), + _ => unimplemented!("asset group weight is not supported yet"), + }; assert_eq!( limiter.divisions(), &[transmuter_math::Division::new($reset_at, $reset_at, value, value).unwrap()] @@ -625,10 +629,10 @@ macro_rules! assert_reset_change_limiters_by_denom { /// This is used for testing if a change limiters for denom has been updated #[cfg(test)] #[macro_export] -macro_rules! assert_dirty_change_limiters_by_denom { - ($denom:expr, $lim:expr, $storage:expr) => { +macro_rules! assert_dirty_change_limiters_by_scope { + ($scope:expr, $lim:expr, $storage:expr) => { let limiters = $lim - .list_limiters_by_denom($storage, $denom) + .list_limiters_by_scope($storage, $scope) .expect("failed to list limiters"); for (label, limiter) in limiters { @@ -639,7 +643,7 @@ macro_rules! assert_dirty_change_limiters_by_denom { limiter, limiter.clone().reset(), "Change Limiter `{}/{}` is clean but expect dirty", - $denom, + $scope, label ); } @@ -668,7 +672,7 @@ mod tests { limiter .register( &mut deps.storage, - "denoma", + Scope::denom("denoma"), "1m", LimiterParams::ChangeLimiter { window_config: WindowConfig { @@ -683,7 +687,7 @@ mod tests { assert_eq!( limiter.list_limiters(&deps.storage).unwrap(), vec![( - ("denoma".to_string(), "1m".to_string()), + (Scope::denom("denoma").key(), "1m".to_string()), Limiter::ChangeLimiter(ChangeLimiter { divisions: vec![], latest_value: Decimal::zero(), @@ -699,7 +703,7 @@ mod tests { limiter .register( &mut deps.storage, - "denoma", + Scope::denom("denoma"), "1h", LimiterParams::ChangeLimiter { window_config: WindowConfig { @@ -715,7 +719,7 @@ mod tests { limiter.list_limiters(&deps.storage).unwrap(), vec![ ( - ("denoma".to_string(), "1h".to_string()), + (Scope::denom("denoma").key(), "1h".to_string()), Limiter::ChangeLimiter(ChangeLimiter { divisions: vec![], latest_value: Decimal::zero(), @@ -727,7 +731,7 @@ mod tests { }) ), ( - ("denoma".to_string(), "1m".to_string()), + (Scope::denom("denoma").key(), "1m".to_string()), Limiter::ChangeLimiter(ChangeLimiter { divisions: vec![], latest_value: Decimal::zero(), @@ -744,7 +748,7 @@ mod tests { limiter .register( &mut deps.storage, - "denomb", + Scope::denom("denomb"), "1m", LimiterParams::ChangeLimiter { window_config: WindowConfig { @@ -760,7 +764,7 @@ mod tests { limiter .register( &mut deps.storage, - "denoma", + Scope::denom("denoma"), "static", LimiterParams::StaticLimiter { upper_limit: Decimal::percent(10), @@ -772,7 +776,7 @@ mod tests { limiter.list_limiters(&deps.storage).unwrap(), vec![ ( - ("denoma".to_string(), "1h".to_string()), + (Scope::denom("denoma").key(), "1h".to_string()), Limiter::ChangeLimiter(ChangeLimiter { divisions: vec![], latest_value: Decimal::zero(), @@ -784,7 +788,7 @@ mod tests { }) ), ( - ("denoma".to_string(), "1m".to_string()), + (Scope::denom("denoma").key(), "1m".to_string()), Limiter::ChangeLimiter(ChangeLimiter { divisions: vec![], latest_value: Decimal::zero(), @@ -796,13 +800,13 @@ mod tests { }) ), ( - ("denoma".to_string(), "static".to_string()), + (Scope::denom("denoma").key(), "static".to_string()), Limiter::StaticLimiter(StaticLimiter { upper_limit: Decimal::percent(10) }) ), ( - ("denomb".to_string(), "1m".to_string()), + (Scope::denom("denomb").key(), "1m".to_string()), Limiter::ChangeLimiter(ChangeLimiter { divisions: vec![], latest_value: Decimal::zero(), @@ -819,7 +823,7 @@ mod tests { // list limiters by denom assert_eq!( limiter - .list_limiters_by_denom(&deps.storage, "denoma") + .list_limiters_by_scope(&deps.storage, &Scope::denom("denoma")) .unwrap(), vec![ ( @@ -857,7 +861,7 @@ mod tests { assert_eq!( limiter - .list_limiters_by_denom(&deps.storage, "denomb") + .list_limiters_by_scope(&deps.storage, &Scope::denom("denomb")) .unwrap(), vec![( "1m".to_string(), @@ -882,7 +886,7 @@ mod tests { let err = limiter .register( &mut deps.storage, - "denoma", + Scope::denom("denoma"), "", LimiterParams::ChangeLimiter { window_config: WindowConfig { @@ -905,7 +909,7 @@ mod tests { limiter .register( &mut deps.storage, - "denoma", + Scope::denom("denoma"), "1m", LimiterParams::ChangeLimiter { window_config: WindowConfig { @@ -920,7 +924,7 @@ mod tests { let err = limiter .register( &mut deps.storage, - "denoma", + Scope::denom("denoma"), "1m", LimiterParams::ChangeLimiter { window_config: WindowConfig { @@ -935,7 +939,7 @@ mod tests { assert_eq!( err, ContractError::LimiterAlreadyExists { - denom: "denoma".to_string(), + scope: Scope::denom("denoma"), label: "1m".to_string() } ); @@ -950,7 +954,7 @@ mod tests { let label = format!("{}h", h); let result = limiter.register( &mut deps.storage, - "denoma", + Scope::denom("denoma"), &label, LimiterParams::ChangeLimiter { window_config: WindowConfig { @@ -967,7 +971,7 @@ mod tests { assert_eq!( result.unwrap_err(), ContractError::MaxLimiterCountPerDenomExceeded { - denom: "denoma".to_string(), + scope: Scope::denom("denoma"), max: MAX_LIMITER_COUNT_PER_DENOM } ); @@ -976,14 +980,14 @@ mod tests { // deregister to register should work limiter - .deregister(&mut deps.storage, "denoma", "1h") + .deregister(&mut deps.storage, Scope::denom("denoma"), "1h") .unwrap(); // register static limiter limiter .register( &mut deps.storage, - "denoma", + Scope::denom("denoma"), "static", LimiterParams::StaticLimiter { upper_limit: Decimal::percent(10), @@ -995,7 +999,7 @@ mod tests { let err = limiter .register( &mut deps.storage, - "denoma", + Scope::denom("denoma"), "static2", LimiterParams::StaticLimiter { upper_limit: Decimal::percent(9), @@ -1006,7 +1010,7 @@ mod tests { assert_eq!( err, ContractError::MaxLimiterCountPerDenomExceeded { - denom: "denoma".to_string(), + scope: Scope::denom("denoma"), max: MAX_LIMITER_COUNT_PER_DENOM } ); @@ -1020,7 +1024,7 @@ mod tests { limiter .register( &mut deps.storage, - "denoma", + Scope::denom("denoma"), "1m", LimiterParams::ChangeLimiter { window_config: WindowConfig { @@ -1035,7 +1039,7 @@ mod tests { assert_eq!( limiter.list_limiters(&deps.storage).unwrap(), vec![( - ("denoma".to_string(), "1m".to_string()), + (Scope::denom("denoma").key(), "1m".to_string()), Limiter::ChangeLimiter(ChangeLimiter { divisions: vec![], latest_value: Decimal::zero(), @@ -1051,7 +1055,7 @@ mod tests { limiter .register( &mut deps.storage, - "denoma", + Scope::denom("denoma"), "1h", LimiterParams::ChangeLimiter { window_config: WindowConfig { @@ -1067,7 +1071,7 @@ mod tests { limiter.list_limiters(&deps.storage).unwrap(), vec![ ( - ("denoma".to_string(), "1h".to_string()), + (Scope::denom("denoma").key(), "1h".to_string()), Limiter::ChangeLimiter(ChangeLimiter { divisions: vec![], latest_value: Decimal::zero(), @@ -1079,7 +1083,7 @@ mod tests { }) ), ( - ("denoma".to_string(), "1m".to_string()), + (Scope::denom("denoma").key(), "1m".to_string()), Limiter::ChangeLimiter(ChangeLimiter { divisions: vec![], latest_value: Decimal::zero(), @@ -1094,25 +1098,25 @@ mod tests { ); let err = limiter - .deregister(&mut deps.storage, "denoma", "nonexistent") + .deregister(&mut deps.storage, Scope::denom("denoma"), "nonexistent") .unwrap_err(); assert_eq!( err, ContractError::LimiterDoesNotExist { - denom: "denoma".to_string(), + scope: Scope::denom("denoma"), label: "nonexistent".to_string(), } ); limiter - .deregister(&mut deps.storage, "denoma", "1m") + .deregister(&mut deps.storage, Scope::denom("denoma"), "1m") .unwrap(); assert_eq!( limiter.list_limiters(&deps.storage).unwrap(), vec![( - ("denoma".to_string(), "1h".to_string()), + (Scope::denom("denoma").key(), "1h".to_string()), Limiter::ChangeLimiter(ChangeLimiter { divisions: vec![], latest_value: Decimal::zero(), @@ -1126,20 +1130,20 @@ mod tests { ); let err = limiter - .deregister(&mut deps.storage, "denoma", "1h") + .deregister(&mut deps.storage, Scope::denom("denoma"), "1h") .unwrap_err(); assert_eq!( err, ContractError::EmptyLimiterNotAllowed { - denom: "denoma".to_string() + scope: Scope::denom("denoma") } ); assert_eq!( limiter.list_limiters(&deps.storage).unwrap(), vec![( - ("denoma".to_string(), "1h".to_string()), + (Scope::denom("denoma").key(), "1h".to_string()), Limiter::ChangeLimiter(ChangeLimiter { divisions: vec![], latest_value: Decimal::zero(), @@ -1154,7 +1158,7 @@ mod tests { limiter .register( &mut deps.storage, - "denomb", + Scope::denom("denomb"), "1m", LimiterParams::ChangeLimiter { window_config: WindowConfig { @@ -1167,20 +1171,20 @@ mod tests { .unwrap(); let err = limiter - .deregister(&mut deps.storage, "denomb", "1m") + .deregister(&mut deps.storage, Scope::denom("denomb"), "1m") .unwrap_err(); assert_eq!( err, ContractError::EmptyLimiterNotAllowed { - denom: "denomb".to_string() + scope: Scope::denom("denomb") } ); limiter .register( &mut deps.storage, - "denomb", + Scope::denom("denomb"), "1h", LimiterParams::ChangeLimiter { window_config: WindowConfig { @@ -1193,25 +1197,25 @@ mod tests { .unwrap(); let err = limiter - .deregister(&mut deps.storage, "denoma", "1h") + .deregister(&mut deps.storage, Scope::denom("denoma"), "1h") .unwrap_err(); assert_eq!( err, ContractError::EmptyLimiterNotAllowed { - denom: "denoma".to_string() + scope: Scope::denom("denoma") } ); limiter - .deregister(&mut deps.storage, "denomb", "1m") + .deregister(&mut deps.storage, Scope::denom("denomb"), "1m") .unwrap(); assert_eq!( limiter.list_limiters(&deps.storage).unwrap(), vec![ ( - ("denoma".to_string(), "1h".to_string()), + (Scope::denom("denoma").key(), "1h".to_string()), Limiter::ChangeLimiter(ChangeLimiter { divisions: vec![], latest_value: Decimal::zero(), @@ -1223,7 +1227,7 @@ mod tests { }) ), ( - ("denomb".to_string(), "1h".to_string()), + (Scope::denom("denomb").key(), "1h".to_string()), Limiter::ChangeLimiter(ChangeLimiter { divisions: vec![], latest_value: Decimal::zero(), @@ -1253,7 +1257,7 @@ mod tests { let err = limiter .register( &mut deps.storage, - "denoma", + Scope::denom("denoma"), "1m", LimiterParams::ChangeLimiter { window_config: WindowConfig { @@ -1277,7 +1281,7 @@ mod tests { let err = limiter .register( &mut deps.storage, - "denoma", + Scope::denom("denoma"), "1m", LimiterParams::ChangeLimiter { window_config: WindowConfig { @@ -1306,7 +1310,7 @@ mod tests { let err = limiter .register( &mut deps.storage, - "denoma", + Scope::denom("denoma"), "1m", LimiterParams::ChangeLimiter { window_config: WindowConfig { @@ -1330,7 +1334,7 @@ mod tests { let err = limiter .register( &mut deps.storage, - "denoma", + Scope::denom("denoma"), "1m", LimiterParams::ChangeLimiter { window_config: WindowConfig { @@ -1359,7 +1363,7 @@ mod tests { limiter .register( &mut deps.storage, - "denoma", + Scope::denom("denoma"), "1m", LimiterParams::ChangeLimiter { window_config: WindowConfig { @@ -1376,7 +1380,7 @@ mod tests { assert_eq!( limiters, vec![( - ("denoma".to_string(), "1m".to_string()), + (Scope::denom("denoma").key(), "1m".to_string()), Limiter::ChangeLimiter(ChangeLimiter { divisions: vec![], latest_value: Decimal::zero(), @@ -1891,7 +1895,7 @@ mod tests { limiter .register( &mut deps.storage, - "denoma", + Scope::denom("denoma"), "1h", LimiterParams::ChangeLimiter { window_config: config, @@ -1906,14 +1910,14 @@ mod tests { limiter .check_limits_and_update( &mut deps.storage, - vec![("denoma".to_string(), (value - EPSILON, value))], + vec![(Scope::denom("denoma"), (value - EPSILON, value))], block_time, ) .unwrap(); // check divs count assert_eq!( - list_divisions(&limiter, "denoma", "1h", &deps.storage).len(), + list_divisions(&limiter, &Scope::denom("denoma"), "1h", &deps.storage).len(), 1 ); @@ -1924,13 +1928,13 @@ mod tests { limiter .check_limits_and_update( &mut deps.storage, - vec![("denoma".to_string(), (value - EPSILON, value))], + vec![(Scope::denom("denoma"), (value - EPSILON, value))], block_time, ) .unwrap(); assert_eq!( - list_divisions(&limiter, "denoma", "1h", &deps.storage).len(), + list_divisions(&limiter, &Scope::denom("denoma"), "1h", &deps.storage).len(), 1 ); @@ -1940,7 +1944,7 @@ mod tests { let err = limiter .check_limits_and_update( &mut deps.storage, - vec![("denoma".to_string(), (value - EPSILON, value))], + vec![(Scope::denom("denoma"), (value - EPSILON, value))], block_time, ) .unwrap_err(); @@ -1948,14 +1952,14 @@ mod tests { assert_eq!( err, ContractError::UpperLimitExceeded { - denom: "denoma".to_string(), + scope: Scope::denom("denoma"), upper_limit: Decimal::percent(58), value: Decimal::from_str("0.580000000000000001").unwrap(), } ); assert_eq!( - list_divisions(&limiter, "denoma", "1h", &deps.storage).len(), + list_divisions(&limiter, &Scope::denom("denoma"), "1h", &deps.storage).len(), 1 ); @@ -1966,7 +1970,7 @@ mod tests { let err = limiter .check_limits_and_update( &mut deps.storage, - vec![("denoma".to_string(), (value - EPSILON, value))], + vec![(Scope::denom("denoma"), (value - EPSILON, value))], block_time, ) .unwrap_err(); @@ -1974,14 +1978,14 @@ mod tests { assert_eq!( err, ContractError::UpperLimitExceeded { - denom: "denoma".to_string(), + scope: Scope::denom("denoma"), upper_limit: Decimal::from_str("0.5875").unwrap(), value: Decimal::from_str("0.587500000000000001").unwrap(), } ); assert_eq!( - list_divisions(&limiter, "denoma", "1h", &deps.storage).len(), + list_divisions(&limiter, &Scope::denom("denoma"), "1h", &deps.storage).len(), 1 ); @@ -1989,13 +1993,13 @@ mod tests { limiter .check_limits_and_update( &mut deps.storage, - vec![("denoma".to_string(), (value - EPSILON, value))], + vec![(Scope::denom("denoma"), (value - EPSILON, value))], block_time, ) .unwrap(); assert_eq!( - list_divisions(&limiter, "denoma", "1h", &deps.storage).len(), + list_divisions(&limiter, &Scope::denom("denoma"), "1h", &deps.storage).len(), 2 ); @@ -2005,7 +2009,7 @@ mod tests { let err = limiter .check_limits_and_update( &mut deps.storage, - vec![("denoma".to_string(), (value - EPSILON, value))], + vec![(Scope::denom("denoma"), (value - EPSILON, value))], block_time, ) .unwrap_err(); @@ -2013,7 +2017,7 @@ mod tests { assert_eq!( err, ContractError::UpperLimitExceeded { - denom: "denoma".to_string(), + scope: Scope::denom("denoma"), upper_limit: Decimal::from_str("0.56").unwrap(), value: Decimal::from_str("0.560000000000000001").unwrap(), } @@ -2026,20 +2030,20 @@ mod tests { let err = limiter .check_limits_and_update( &mut deps.storage, - vec![("denoma".to_string(), (value - EPSILON, value))], + vec![(Scope::denom("denoma"), (value - EPSILON, value))], block_time, ) .unwrap_err(); assert_eq!( - list_divisions(&limiter, "denoma", "1h", &deps.storage).len(), + list_divisions(&limiter, &Scope::denom("denoma"), "1h", &deps.storage).len(), 2 ); assert_eq!( err, ContractError::UpperLimitExceeded { - denom: "denoma".to_string(), + scope: Scope::denom("denoma"), upper_limit: Decimal::from_str("0.525").unwrap(), value: Decimal::from_str("0.525000000000000001").unwrap(), } @@ -2049,13 +2053,13 @@ mod tests { limiter .check_limits_and_update( &mut deps.storage, - vec![("denoma".to_string(), (value - EPSILON, value))], + vec![(Scope::denom("denoma"), (value - EPSILON, value))], block_time, ) .unwrap(); assert_eq!( - list_divisions(&limiter, "denoma", "1h", &deps.storage).len(), + list_divisions(&limiter, &Scope::denom("denoma"), "1h", &deps.storage).len(), 3 ); } @@ -2071,7 +2075,7 @@ mod tests { limiter .register( &mut deps.storage, - "denomb", + Scope::denom("denomb"), "1h", LimiterParams::ChangeLimiter { window_config: config, @@ -2083,7 +2087,7 @@ mod tests { limiter .set_change_limiter_boundary_offset( &mut deps.storage, - "denomb", + Scope::denom("denomb"), "1h", Decimal::percent(5), ) @@ -2095,13 +2099,13 @@ mod tests { limiter .check_limits_and_update( &mut deps.storage, - vec![("denomb".to_string(), (value - EPSILON, value))], + vec![(Scope::denom("denomb"), (value - EPSILON, value))], block_time, ) .unwrap(); assert_eq!( - list_divisions(&limiter, "denomb", "1h", &deps.storage).len(), + list_divisions(&limiter, &Scope::denom("denomb"), "1h", &deps.storage).len(), 1 ); @@ -2110,13 +2114,13 @@ mod tests { limiter .check_limits_and_update( &mut deps.storage, - vec![("denomb".to_string(), (value - EPSILON, value))], + vec![(Scope::denom("denomb"), (value - EPSILON, value))], block_time, ) .unwrap(); assert_eq!( - list_divisions(&limiter, "denomb", "1h", &deps.storage).len(), + list_divisions(&limiter, &Scope::denom("denomb"), "1h", &deps.storage).len(), 1 ); @@ -2125,20 +2129,20 @@ mod tests { let err = limiter .check_limits_and_update( &mut deps.storage, - vec![("denomb".to_string(), (value - EPSILON, value))], + vec![(Scope::denom("denomb"), (value - EPSILON, value))], block_time, ) .unwrap_err(); assert_eq!( - list_divisions(&limiter, "denomb", "1h", &deps.storage).len(), + list_divisions(&limiter, &Scope::denom("denomb"), "1h", &deps.storage).len(), 1 ); assert_eq!( err, ContractError::UpperLimitExceeded { - denom: "denomb".to_string(), + scope: Scope::denom("denomb"), upper_limit: Decimal::from_str("0.5").unwrap(), value: Decimal::from_str("0.500000000000000001").unwrap(), } @@ -2148,14 +2152,14 @@ mod tests { limiter .check_limits_and_update( &mut deps.storage, - vec![("denomb".to_string(), (value - EPSILON, value))], + vec![(Scope::denom("denomb"), (value - EPSILON, value))], block_time, ) .unwrap(); // 1st division stiil there assert_eq!( - list_divisions(&limiter, "denomb", "1h", &deps.storage).len(), + list_divisions(&limiter, &Scope::denom("denomb"), "1h", &deps.storage).len(), 2 ); @@ -2164,7 +2168,7 @@ mod tests { let err = limiter .check_limits_and_update( &mut deps.storage, - vec![("denomb".to_string(), (value - EPSILON, value))], + vec![(Scope::denom("denomb"), (value - EPSILON, value))], block_time, ) .unwrap_err(); @@ -2172,7 +2176,7 @@ mod tests { assert_eq!( err, ContractError::UpperLimitExceeded { - denom: "denomb".to_string(), + scope: Scope::denom("denomb"), upper_limit: Decimal::from_str("0.491666666666666666").unwrap(), value: Decimal::from_str("0.491666666666666667").unwrap(), } @@ -2180,23 +2184,23 @@ mod tests { // 1st division is not removed yet since limit exceeded first assert_eq!( - list_divisions(&limiter, "denomb", "1h", &deps.storage).len(), + list_divisions(&limiter, &Scope::denom("denomb"), "1h", &deps.storage).len(), 2 ); - let old_divs = list_divisions(&limiter, "denomb", "1h", &deps.storage); + let old_divs = list_divisions(&limiter, &Scope::denom("denomb"), "1h", &deps.storage); let value = Decimal::from_str("0.491666666666666666").unwrap(); limiter .check_limits_and_update( &mut deps.storage, - vec![("denomb".to_string(), (value - EPSILON, value))], + vec![(Scope::denom("denomb"), (value - EPSILON, value))], block_time, ) .unwrap(); // 1st division is removed, and add new division assert_eq!( - list_divisions(&limiter, "denomb", "1h", &deps.storage), + list_divisions(&limiter, &Scope::denom("denomb"), "1h", &deps.storage), [ old_divs[1..].to_vec(), vec![Division::new( @@ -2222,7 +2226,7 @@ mod tests { limiter .register( &mut deps.storage, - "denomb", + Scope::denom("denomb"), "1h", LimiterParams::ChangeLimiter { window_config: config, @@ -2234,7 +2238,7 @@ mod tests { limiter .set_change_limiter_boundary_offset( &mut deps.storage, - "denomb", + Scope::denom("denomb"), "1h", Decimal::percent(5), ) @@ -2245,7 +2249,7 @@ mod tests { limiter .check_limits_and_update( &mut deps.storage, - vec![("denomb".to_string(), (value - EPSILON, value))], + vec![(Scope::denom("denomb"), (value - EPSILON, value))], block_time, ) .unwrap(); @@ -2255,7 +2259,7 @@ mod tests { limiter .check_limits_and_update( &mut deps.storage, - vec![("denomb".to_string(), (value - EPSILON, value))], + vec![(Scope::denom("denomb"), (value - EPSILON, value))], block_time, ) .unwrap(); @@ -2265,7 +2269,7 @@ mod tests { limiter .check_limits_and_update( &mut deps.storage, - vec![("denomb".to_string(), (value - EPSILON, value))], + vec![(Scope::denom("denomb"), (value - EPSILON, value))], block_time, ) .unwrap(); @@ -2276,7 +2280,7 @@ mod tests { let err = limiter .check_limits_and_update( &mut deps.storage, - vec![("denomb".to_string(), (value - EPSILON, value))], + vec![(Scope::denom("denomb"), (value - EPSILON, value))], block_time, ) .unwrap_err(); @@ -2284,7 +2288,7 @@ mod tests { assert_eq!( err, ContractError::UpperLimitExceeded { - denom: String::from("denomb"), + scope: Scope::denom("denomb"), upper_limit: Decimal::percent(51), value } @@ -2299,7 +2303,7 @@ mod tests { limiter .register( &mut deps.storage, - "denom", + Scope::denom("denom"), "1h", LimiterParams::ChangeLimiter { window_config: WindowConfig { @@ -2318,7 +2322,7 @@ mod tests { limiter .check_limits_and_update( &mut deps.storage, - vec![("denom".to_string(), (Decimal::zero(), value))], + vec![(Scope::denom("denom"), (Decimal::zero(), value))], block_time, ) .unwrap(); @@ -2329,7 +2333,7 @@ mod tests { let err = limiter .check_limits_and_update( &mut deps.storage, - vec![("denom".to_string(), (value, new_value))], + vec![(Scope::denom("denom"), (value, new_value))], new_block_time, ) .unwrap_err(); @@ -2337,7 +2341,7 @@ mod tests { assert_eq!( err, ContractError::UpperLimitExceeded { - denom: "denom".to_string(), + scope: Scope::denom("denom"), upper_limit: Decimal::percent(56), value: new_value, } @@ -2349,7 +2353,7 @@ mod tests { limiter .check_limits_and_update( &mut deps.storage, - vec![("denom".to_string(), (value, new_value))], + vec![(Scope::denom("denom"), (value, new_value))], new_block_time, ) .unwrap(); @@ -2362,7 +2366,7 @@ mod tests { limiter .check_limits_and_update( &mut deps.storage, - vec![("denom".to_string(), (value, new_value))], + vec![(Scope::denom("denom"), (value, new_value))], new_block_time, ) .unwrap(); @@ -2375,7 +2379,7 @@ mod tests { limiter .check_limits_and_update( &mut deps.storage, - vec![("denom".to_string(), (value, final_value))], + vec![(Scope::denom("denom"), (value, final_value))], final_block_time, ) .unwrap(); @@ -2386,7 +2390,7 @@ mod tests { let err = limiter .check_limits_and_update( &mut deps.storage, - vec![("denom".to_string(), (value, new_value))], + vec![(Scope::denom("denom"), (value, new_value))], final_block_time, ) .unwrap_err(); @@ -2394,7 +2398,7 @@ mod tests { assert_eq!( err, ContractError::UpperLimitExceeded { - denom: "denom".to_string(), + scope: Scope::denom("denom"), upper_limit: Decimal::from_str("0.555").unwrap(), value: new_value, } @@ -2409,7 +2413,7 @@ mod tests { limiter .register( &mut deps.storage, - "denoma", + Scope::denom("denoma"), "1h", LimiterParams::StaticLimiter { upper_limit: Decimal::percent(60), @@ -2420,7 +2424,7 @@ mod tests { limiter .register( &mut deps.storage, - "denomb", + Scope::denom("denomb"), "1h", LimiterParams::StaticLimiter { upper_limit: Decimal::percent(70), @@ -2436,8 +2440,8 @@ mod tests { .check_limits_and_update( &mut deps.storage, vec![ - ("denoma".to_string(), (value_a - EPSILON, value_a)), - ("denomb".to_string(), (value_b + EPSILON, value_b)), + (Scope::denom("denoma"), (value_a - EPSILON, value_a)), + (Scope::denom("denomb"), (value_b + EPSILON, value_b)), ], block_time, ) @@ -2450,8 +2454,8 @@ mod tests { .check_limits_and_update( &mut deps.storage, vec![ - ("denoma".to_string(), (value_a - EPSILON, value_a)), - ("denomb".to_string(), (value_b + EPSILON, value_b)), + (Scope::denom("denoma"), (value_a - EPSILON, value_a)), + (Scope::denom("denomb"), (value_b + EPSILON, value_b)), ], block_time, ) @@ -2460,7 +2464,7 @@ mod tests { assert_eq!( err, ContractError::UpperLimitExceeded { - denom: "denoma".to_string(), + scope: Scope::denom("denoma"), upper_limit: Decimal::from_str("0.6").unwrap(), value: Decimal::from_str("0.600000000000000001").unwrap(), } @@ -2473,8 +2477,8 @@ mod tests { .check_limits_and_update( &mut deps.storage, vec![ - ("denoma".to_string(), (value_a + EPSILON, value_a)), - ("denomb".to_string(), (value_b - EPSILON, value_b)), + (Scope::denom("denoma"), (value_a + EPSILON, value_a)), + (Scope::denom("denomb"), (value_b - EPSILON, value_b)), ], block_time, ) @@ -2483,7 +2487,7 @@ mod tests { assert_eq!( err, ContractError::UpperLimitExceeded { - denom: "denomb".to_string(), + scope: Scope::denom("denomb"), upper_limit: Decimal::from_str("0.7").unwrap(), value: Decimal::from_str("0.700000000000000001").unwrap(), } @@ -2496,8 +2500,8 @@ mod tests { .check_limits_and_update( &mut deps.storage, vec![ - ("denoma".to_string(), (value_a - EPSILON, value_a)), - ("denomb".to_string(), (value_b + EPSILON, value_b)), + (Scope::denom("denoma"), (value_a - EPSILON, value_a)), + (Scope::denom("denomb"), (value_b + EPSILON, value_b)), ], block_time, ) @@ -2510,8 +2514,8 @@ mod tests { .check_limits_and_update( &mut deps.storage, vec![ - ("denoma".to_string(), (value_a - EPSILON, value_a)), - ("denomb".to_string(), (value_b + EPSILON, value_b)), + (Scope::denom("denoma"), (value_a - EPSILON, value_a)), + (Scope::denom("denomb"), (value_b + EPSILON, value_b)), ], block_time, ) @@ -2529,8 +2533,8 @@ mod tests { .check_limits_and_update( &mut deps.storage, vec![ - ("denoma".to_string(), (value_a, new_value_a)), - ("denomb".to_string(), (value_b, new_value_b)), + (Scope::denom("denoma"), (value_a, new_value_a)), + (Scope::denom("denomb"), (value_b, new_value_b)), ], block_time, ) @@ -2548,8 +2552,8 @@ mod tests { .check_limits_and_update( &mut deps.storage, vec![ - ("denoma".to_string(), (value_a, new_value_a)), - ("denomb".to_string(), (value_b, new_value_b)), + (Scope::denom("denoma"), (value_a, new_value_a)), + (Scope::denom("denomb"), (value_b, new_value_b)), ], block_time, ) @@ -2574,7 +2578,7 @@ mod tests { limiter .register( &mut deps.storage, - "denoma", + Scope::denom("denoma"), "1h", LimiterParams::ChangeLimiter { window_config: config_1h.clone(), @@ -2586,7 +2590,7 @@ mod tests { limiter .register( &mut deps.storage, - "denoma", + Scope::denom("denoma"), "1w", LimiterParams::ChangeLimiter { window_config: config_1w.clone(), @@ -2598,7 +2602,7 @@ mod tests { limiter .register( &mut deps.storage, - "denomb", + Scope::denom("denomb"), "1h", LimiterParams::ChangeLimiter { window_config: config_1h, @@ -2610,7 +2614,7 @@ mod tests { limiter .register( &mut deps.storage, - "denomb", + Scope::denom("denomb"), "1w", LimiterParams::ChangeLimiter { window_config: config_1w, @@ -2622,7 +2626,7 @@ mod tests { limiter .register( &mut deps.storage, - "denomb", + Scope::denom("denomb"), "static", LimiterParams::StaticLimiter { upper_limit: Decimal::percent(55), @@ -2638,8 +2642,8 @@ mod tests { .check_limits_and_update( &mut deps.storage, vec![ - ("denoma".to_string(), (value_a, value_a)), - ("denomb".to_string(), (value_b, value_b)), + (Scope::denom("denoma"), (value_a, value_a)), + (Scope::denom("denomb"), (value_b, value_b)), ], block_time, ) @@ -2652,9 +2656,9 @@ mod tests { .check_limits_and_update( &mut deps.storage, vec![ - ("denoma".to_string(), (value - EPSILON, value)), + (Scope::denom("denoma"), (value - EPSILON, value)), ( - "denomb".to_string(), + Scope::denom("denomb"), (Decimal::one() - value + EPSILON, Decimal::one() - value), ), ], @@ -2665,7 +2669,7 @@ mod tests { assert_eq!( err, ContractError::UpperLimitExceeded { - denom: "denoma".to_string(), + scope: Scope::denom("denoma"), upper_limit: Decimal::from_str("0.6").unwrap(), value: Decimal::from_str("0.600000000000000001").unwrap(), } @@ -2678,8 +2682,8 @@ mod tests { .check_limits_and_update( &mut deps.storage, vec![ - ("denoma".to_string(), (value_a + EPSILON, value_a)), - ("denomb".to_string(), (value_b - EPSILON, value_b)), + (Scope::denom("denoma"), (value_a + EPSILON, value_a)), + (Scope::denom("denomb"), (value_b - EPSILON, value_b)), ], block_time, ) @@ -2688,7 +2692,7 @@ mod tests { assert_eq!( err, ContractError::UpperLimitExceeded { - denom: "denomb".to_string(), + scope: Scope::denom("denomb"), upper_limit: Decimal::from_str("0.55").unwrap(), value: Decimal::from_str("0.550000000000000001").unwrap(), } @@ -2701,8 +2705,8 @@ mod tests { .check_limits_and_update( &mut deps.storage, vec![ - ("denoma".to_string(), (value_a, value_a)), - ("denomb".to_string(), (value_b, value_b)), + (Scope::denom("denoma"), (value_a, value_a)), + (Scope::denom("denomb"), (value_b, value_b)), ], block_time, ) @@ -2718,8 +2722,8 @@ mod tests { .check_limits_and_update( &mut deps.storage, vec![ - ("denoma".to_string(), (value_a - EPSILON, value_a)), - ("denomb".to_string(), (value_b + EPSILON, value_b)), + (Scope::denom("denoma"), (value_a - EPSILON, value_a)), + (Scope::denom("denomb"), (value_b + EPSILON, value_b)), ], block_time, ) @@ -2728,7 +2732,7 @@ mod tests { assert_eq!( err, ContractError::UpperLimitExceeded { - denom: "denoma".to_string(), + scope: Scope::denom("denoma"), upper_limit: Decimal::from_str("0.525").unwrap(), value: Decimal::from_str("0.525000000000000001").unwrap(), } @@ -2741,8 +2745,8 @@ mod tests { .check_limits_and_update( &mut deps.storage, vec![ - ("denoma".to_string(), (value_a - EPSILON, value_a)), - ("denomb".to_string(), (value_b + EPSILON, value_b)), + (Scope::denom("denoma"), (value_a - EPSILON, value_a)), + (Scope::denom("denomb"), (value_b + EPSILON, value_b)), ], block_time, ) @@ -2751,7 +2755,7 @@ mod tests { assert_eq!( err, ContractError::UpperLimitExceeded { - denom: "denoma".to_string(), + scope: Scope::denom("denoma"), upper_limit: Decimal::from_str("0.55").unwrap(), value: Decimal::from_str("0.550000000000000001").unwrap(), } @@ -2772,7 +2776,7 @@ mod tests { limiters .register( &mut deps.storage, - "denomc", + Scope::denom("denomc"), "1h", LimiterParams::ChangeLimiter { window_config: config, @@ -2784,7 +2788,7 @@ mod tests { limiters .register( &mut deps.storage, - "denomc", + Scope::denom("denomc"), "static", LimiterParams::StaticLimiter { upper_limit: Decimal::percent(60), @@ -2795,7 +2799,7 @@ mod tests { limiters .set_change_limiter_boundary_offset( &mut deps.storage, - "denomc", + Scope::denom("denomc"), "1h", Decimal::percent(20), ) @@ -2803,7 +2807,7 @@ mod tests { let limiter = match limiters .limiters - .load(&deps.storage, ("denomc", "1h")) + .load(&deps.storage, (&Scope::denom("denomc").key(), "1h")) .unwrap() { Limiter::ChangeLimiter(limiter) => limiter, @@ -2817,7 +2821,7 @@ mod tests { let err = limiters .set_change_limiter_boundary_offset( &mut deps.storage, - "denomc", + Scope::denom("denomc"), "static", Decimal::percent(20), ) @@ -2834,7 +2838,7 @@ mod tests { let err = limiters .set_change_limiter_boundary_offset( &mut deps.storage, - "denomc", + Scope::denom("denomc"), "1h", Decimal::zero(), ) @@ -2854,7 +2858,7 @@ mod tests { limiters .register( &mut deps.storage, - "denomc", + Scope::denom("denomc"), "1h", LimiterParams::ChangeLimiter { window_config: config, @@ -2866,7 +2870,7 @@ mod tests { limiters .register( &mut deps.storage, - "denomc", + Scope::denom("denomc"), "static", LimiterParams::StaticLimiter { upper_limit: Decimal::percent(60), @@ -2878,7 +2882,7 @@ mod tests { limiters .set_static_limiter_upper_limit( &mut deps.storage, - "denomc", + Scope::denom("denomc"), "static", upper_limit, ) @@ -2886,7 +2890,7 @@ mod tests { let limiter = match limiters .limiters - .load(&deps.storage, ("denomc", "static")) + .load(&deps.storage, (&Scope::denom("denomc").key(), "static")) .unwrap() { Limiter::StaticLimiter(limiter) => limiter, @@ -2896,7 +2900,12 @@ mod tests { assert_eq!(limiter.upper_limit, upper_limit); let err = limiters - .set_static_limiter_upper_limit(&mut deps.storage, "denomc", "1h", upper_limit) + .set_static_limiter_upper_limit( + &mut deps.storage, + Scope::denom("denomc"), + "1h", + upper_limit, + ) .unwrap_err(); assert_eq!( @@ -2934,7 +2943,7 @@ mod tests { limiters .register( &mut deps.storage, - "denoma", + Scope::denom("denoma"), "1h", LimiterParams::ChangeLimiter { window_config: config_1h.clone(), @@ -2946,7 +2955,7 @@ mod tests { limiters .register( &mut deps.storage, - "denoma", + Scope::denom("denoma"), "1w", LimiterParams::ChangeLimiter { window_config: config_1w.clone(), @@ -2962,7 +2971,7 @@ mod tests { limiters .check_limits_and_update( &mut deps.storage, - vec![("denoma".to_string(), (value - EPSILON, value))], + vec![(Scope::denom("denoma"), (value - EPSILON, value))], block_time, ) .unwrap(); @@ -2974,8 +2983,12 @@ mod tests { .unwrap(); for (denom, window) in keys.iter() { - let divisions = - list_divisions(&limiters, denom.as_str(), window.as_str(), &deps.storage); + let divisions = list_divisions( + &limiters, + &denom.parse().unwrap(), + window.as_str(), + &deps.storage, + ); assert_eq!( divisions, @@ -2983,7 +2996,11 @@ mod tests { ) } - assert_dirty_change_limiters_by_denom!("denoma", &limiters, &deps.storage); + assert_dirty_change_limiters_by_scope!( + &Scope::denom("denoma"), + &limiters, + &deps.storage + ); // reset limiters let block_time = block_time.plus_hours(1); @@ -2992,13 +3009,17 @@ mod tests { .reset_change_limiter_states( &mut deps.storage, block_time, - vec![("denoma".to_string(), value)], + vec![(Scope::denom("denoma").key(), value)], ) .unwrap(); for (denom, window) in keys.iter() { - let divisions = - list_divisions(&limiters, denom.as_str(), window.as_str(), &deps.storage); + let divisions = list_divisions( + &limiters, + &denom.parse().unwrap(), + window.as_str(), + &deps.storage, + ); assert_eq!( divisions, @@ -3010,11 +3031,15 @@ mod tests { fn list_divisions( limiters: &Limiters, - denom: &str, + scope: &Scope, window: &str, storage: &dyn Storage, ) -> Vec { - match limiters.limiters.load(storage, (denom, window)).unwrap() { + match limiters + .limiters + .load(storage, (&scope.key(), window)) + .unwrap() + { Limiter::ChangeLimiter(limiter) => limiter.divisions, Limiter::StaticLimiter(_) => panic!("not a change limiter"), } diff --git a/contracts/transmuter/src/scope.rs b/contracts/transmuter/src/scope.rs new file mode 100644 index 0000000..746c197 --- /dev/null +++ b/contracts/transmuter/src/scope.rs @@ -0,0 +1,57 @@ +use cosmwasm_schema::cw_serde; +use std::{fmt::Display, str::FromStr}; + +/// Scope for configuring limiters & rebalacing incentive for +#[cw_serde] +#[serde(tag = "type", content = "value")] +pub enum Scope { + Denom(String), + AssetGroup(String), +} + +impl Display for Scope { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.key()) + } +} + +#[derive(Debug, thiserror::Error)] +#[error("Invalid scope: {0}, must start with 'denom::' or 'asset_group::'")] +pub struct ParseScopeErr(String); + +impl FromStr for Scope { + type Err = ParseScopeErr; + + fn from_str(s: &str) -> Result { + if s.starts_with("denom::") { + s.strip_prefix("denom::") + .map(|s| Scope::Denom(s.to_string())) + .ok_or(ParseScopeErr(s.to_string())) + } else if s.starts_with("asset_group::") { + s.strip_prefix("asset_group::") + .map(|s| Scope::AssetGroup(s.to_string())) + .ok_or(ParseScopeErr(s.to_string())) + } else { + Err(ParseScopeErr(s.to_string())) + } + } +} + +impl Scope { + pub fn key(&self) -> String { + match self { + Scope::Denom(denom) => format!("denom::{}", denom), + Scope::AssetGroup(label) => format!("asset_group::{}", label), + } + } +} + +impl Scope { + pub fn denom(denom: &str) -> Self { + Scope::Denom(denom.to_string()) + } + + pub fn asset_group(label: &str) -> Self { + Scope::AssetGroup(label.to_string()) + } +} diff --git a/contracts/transmuter/src/swap.rs b/contracts/transmuter/src/swap.rs index 88348a9..aa818c8 100644 --- a/contracts/transmuter/src/swap.rs +++ b/contracts/transmuter/src/swap.rs @@ -11,6 +11,7 @@ use serde::Serialize; use crate::{ alloyed_asset::{swap_from_alloyed, swap_to_alloyed}, contract::Transmuter, + scope::Scope, transmuter_pool::{AmountConstraint, TransmuterPool}, ContractError, }; @@ -293,7 +294,11 @@ impl Transmuter<'_> { self.limiters.reset_change_limiter_states( deps.storage, env.block.time, - pool.weights()?.unwrap_or_default(), + pool.weights()? + .unwrap_or_default() + .into_iter() + .map(|(denom, weight)| (Scope::denom(&denom).key(), weight)) // TODO: handle asset group + .collect::>(), )?; } else { let prev_weights = pool.weights_map()?; @@ -588,8 +593,12 @@ impl Transmuter<'_> { for corrupted in pool.clone().corrupted_assets() { if corrupted.amount().is_zero() { pool.remove_corrupted_asset(corrupted.denom())?; - self.limiters - .uncheck_deregister_all_for_denom(storage, corrupted.denom())?; + self.limiters.uncheck_deregister_all_for_scope( + storage, + Scope::denom(corrupted.denom()), // TODO: bubble this up + )?; + + // TODO: remove denom from asset group, if asset group is empty, remove it } } @@ -597,15 +606,16 @@ impl Transmuter<'_> { } } +// TODO: compute weight pairs by target > denom fn pair_weights_by_denom( prev_weights: BTreeMap, updated_weights: Vec<(String, Decimal)>, -) -> Vec<(String, (Decimal, Decimal))> { +) -> Vec<(Scope, (Decimal, Decimal))> { let mut denom_weight_pairs = Vec::new(); for (denom, weight) in updated_weights { let prev_weight = prev_weights.get(denom.as_str()).unwrap_or(&weight); - denom_weight_pairs.push((denom, (*prev_weight, weight))); + denom_weight_pairs.push((Scope::denom(&denom), (*prev_weight, weight))); } denom_weight_pairs @@ -1184,7 +1194,7 @@ mod tests { .limiters .register( &mut deps.storage, - denom.as_str(), + Scope::denom(denom.as_str()), "static", LimiterParams::StaticLimiter { upper_limit: Decimal::percent(100), @@ -1219,7 +1229,7 @@ mod tests { assert!( transmuter .limiters - .list_limiters_by_denom(&deps.storage, denom.as_str()) + .list_limiters_by_scope(&deps.storage, &Scope::denom(denom.as_str())) .unwrap() .is_empty(), "must not contain limiter for {} since it's corrupted and drained", @@ -1236,7 +1246,7 @@ mod tests { assert!( !transmuter .limiters - .list_limiters_by_denom(&deps.storage, denom.as_str()) + .list_limiters_by_scope(&deps.storage, &Scope::denom(denom.as_str())) .unwrap() .is_empty(), "must contain limiter for {} since it's not corrupted or not drained", @@ -1284,7 +1294,7 @@ mod tests { .limiters .register( &mut deps.storage, - denom.as_str(), + Scope::denom(denom.as_str()), "static", LimiterParams::StaticLimiter { upper_limit: Decimal::percent(100), @@ -1324,7 +1334,10 @@ mod tests { .unique() .collect_vec(); - assert_eq!(limiter_denoms, vec!["denom2", "denom3"]); + assert_eq!( + limiter_denoms, + vec![Scope::denom("denom2").key(), Scope::denom("denom3").key()] + ); } #[test] @@ -1365,7 +1378,7 @@ mod tests { .limiters .register( &mut deps.storage, - denom.as_str(), + Scope::denom(denom.as_str()), "static", LimiterParams::StaticLimiter { upper_limit: Decimal::percent(100), @@ -1405,7 +1418,10 @@ mod tests { .unique() .collect_vec(); - assert_eq!(limiter_denoms, vec!["denom2", "denom3"]); + assert_eq!( + limiter_denoms, + vec![Scope::denom("denom2").key(), Scope::denom("denom3").key()] + ); } #[rstest] diff --git a/contracts/transmuter/src/test/cases/scenarios.rs b/contracts/transmuter/src/test/cases/scenarios.rs index e487955..584dba5 100644 --- a/contracts/transmuter/src/test/cases/scenarios.rs +++ b/contracts/transmuter/src/test/cases/scenarios.rs @@ -2,13 +2,13 @@ use std::{str::FromStr, vec}; use crate::{ asset::AssetConfig, - contract::sv::QueryMsg, - contract::sv::{ExecMsg, InstantiateMsg}, contract::{ + sv::{ExecMsg, InstantiateMsg, QueryMsg}, GetShareDenomResponse, GetSharesResponse, GetTotalPoolLiquidityResponse, GetTotalSharesResponse, ListLimitersResponse, }, limiter::{ChangeLimiter, Limiter, LimiterParams, StaticLimiter, WindowConfig}, + scope::Scope, test::{ modules::cosmwasm_pool::CosmwasmPool, test_env::{assert_contract_err, TestEnvBuilder}, @@ -992,7 +992,7 @@ fn test_limiters() { t.contract .execute( &ExecMsg::RegisterLimiter { - denom: AXL_USDC.to_string(), + scope: Scope::Denom(AXL_USDC.to_string()), label: "1h".to_string(), limiter_params: LimiterParams::ChangeLimiter { window_config: config_1h.clone(), @@ -1007,7 +1007,7 @@ fn test_limiters() { t.contract .execute( &ExecMsg::RegisterLimiter { - denom: AXL_USDC.to_string(), + scope: Scope::Denom(AXL_USDC.to_string()), label: "1w".to_string(), limiter_params: LimiterParams::ChangeLimiter { window_config: config_1w.clone(), @@ -1022,7 +1022,7 @@ fn test_limiters() { t.contract .execute( &ExecMsg::RegisterLimiter { - denom: COSMOS_USDC.to_string(), + scope: Scope::Denom(COSMOS_USDC.to_string()), label: "1h".to_string(), limiter_params: LimiterParams::ChangeLimiter { window_config: config_1h.clone(), @@ -1037,7 +1037,7 @@ fn test_limiters() { t.contract .execute( &ExecMsg::RegisterLimiter { - denom: COSMOS_USDC.to_string(), + scope: Scope::Denom(COSMOS_USDC.to_string()), label: "1w".to_string(), limiter_params: LimiterParams::ChangeLimiter { window_config: config_1w.clone(), @@ -1052,7 +1052,7 @@ fn test_limiters() { t.contract .execute( &ExecMsg::RegisterLimiter { - denom: COSMOS_USDC.to_string(), + scope: Scope::Denom(COSMOS_USDC.to_string()), label: "static".to_string(), limiter_params: LimiterParams::StaticLimiter { upper_limit: Decimal::percent(55), @@ -1070,29 +1070,29 @@ fn test_limiters() { limiters, vec![ ( - (AXL_USDC.to_string(), "1h".to_string()), + (Scope::denom(AXL_USDC).key(), "1h".to_string()), Limiter::ChangeLimiter( ChangeLimiter::new(config_1h.clone(), Decimal::percent(10)).unwrap() ) ), ( - (AXL_USDC.to_string(), "1w".to_string()), + (Scope::denom(AXL_USDC).key(), "1w".to_string()), Limiter::ChangeLimiter( ChangeLimiter::new(config_1w.clone(), Decimal::percent(5)).unwrap() ) ), ( - (COSMOS_USDC.to_string(), "1h".to_string()), + (Scope::denom(COSMOS_USDC).key(), "1h".to_string()), Limiter::ChangeLimiter( ChangeLimiter::new(config_1h, Decimal::percent(10)).unwrap() ) ), ( - (COSMOS_USDC.to_string(), "1w".to_string()), + (Scope::denom(COSMOS_USDC).key(), "1w".to_string()), Limiter::ChangeLimiter(ChangeLimiter::new(config_1w, Decimal::percent(5)).unwrap()) ), ( - (COSMOS_USDC.to_string(), "static".to_string()), + (Scope::denom(COSMOS_USDC).key(), "static".to_string()), Limiter::StaticLimiter(StaticLimiter::new(Decimal::percent(55)).unwrap()) ), ] @@ -1130,7 +1130,7 @@ fn test_limiters() { assert_contract_err( ContractError::UpperLimitExceeded { - denom: AXL_USDC.to_string(), + scope: Scope::denom(AXL_USDC), upper_limit: Decimal::from_str("0.6").unwrap(), value: Decimal::from_str("0.600001").unwrap(), }, @@ -1155,7 +1155,7 @@ fn test_limiters() { assert_contract_err( ContractError::UpperLimitExceeded { - denom: COSMOS_USDC.to_string(), + scope: Scope::denom(COSMOS_USDC), upper_limit: Decimal::from_str("0.55").unwrap(), value: Decimal::from_str("0.550001").unwrap(), }, @@ -1193,7 +1193,7 @@ fn test_limiters() { assert_contract_err( ContractError::UpperLimitExceeded { - denom: AXL_USDC.to_string(), + scope: Scope::denom(AXL_USDC), upper_limit: Decimal::from_str("0.55").unwrap(), value: Decimal::from_str("0.5625").unwrap(), }, @@ -1212,7 +1212,7 @@ fn test_limiters() { assert_contract_err( ContractError::UpperLimitExceeded { - denom: AXL_USDC.to_string(), + scope: Scope::denom(AXL_USDC), upper_limit: Decimal::from_str("0.525034626038781163").unwrap(), value: Decimal::from_str("0.5416666666666666").unwrap(), }, @@ -1243,7 +1243,7 @@ fn test_limiters() { assert_contract_err( ContractError::UpperLimitExceeded { - denom: COSMOS_USDC.to_string(), + scope: Scope::denom(COSMOS_USDC), upper_limit: Decimal::from_str("0.65").unwrap(), value: Decimal::from_str("0.6875").unwrap(), }, @@ -1268,7 +1268,7 @@ fn test_limiters() { assert_contract_err( ContractError::UpperLimitExceeded { - denom: COSMOS_USDC.to_string(), + scope: Scope::denom(COSMOS_USDC), upper_limit: Decimal::from_str("0.575").unwrap(), value: Decimal::from_str("0.625").unwrap(), }, @@ -1343,7 +1343,7 @@ fn test_register_limiter_after_having_liquidity() { t.contract .execute( &ExecMsg::RegisterLimiter { - denom: COSMOS_USDC.to_string(), + scope: Scope::Denom(COSMOS_USDC.to_string()), label: "static".to_string(), limiter_params: LimiterParams::StaticLimiter { upper_limit: Decimal::percent(60), @@ -1371,7 +1371,7 @@ fn test_register_limiter_after_having_liquidity() { assert_contract_err( ContractError::UpperLimitExceeded { - denom: COSMOS_USDC.to_string(), + scope: Scope::denom(COSMOS_USDC), upper_limit: Decimal::from_str("0.6").unwrap(), value: Decimal::from_str("1").unwrap(), }, @@ -1441,7 +1441,7 @@ fn test_register_limiter_after_having_liquidity() { assert_contract_err( ContractError::UpperLimitExceeded { - denom: COSMOS_USDC.to_string(), + scope: Scope::denom(COSMOS_USDC), upper_limit: Decimal::from_str("0.6").unwrap(), value: Decimal::from_str("0.999995").unwrap(), }, From ccd9ff439c63ddaf450f4b759f231b96a8066edd Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Wed, 25 Sep 2024 13:31:34 +0700 Subject: [PATCH 02/26] construct scope value pairs --- contracts/transmuter/src/scope.rs | 1 + contracts/transmuter/src/swap.rs | 172 +++++++++++++++++++++++++++--- 2 files changed, 158 insertions(+), 15 deletions(-) diff --git a/contracts/transmuter/src/scope.rs b/contracts/transmuter/src/scope.rs index 746c197..9ee3391 100644 --- a/contracts/transmuter/src/scope.rs +++ b/contracts/transmuter/src/scope.rs @@ -4,6 +4,7 @@ use std::{fmt::Display, str::FromStr}; /// Scope for configuring limiters & rebalacing incentive for #[cw_serde] #[serde(tag = "type", content = "value")] +#[derive(Eq, Hash)] pub enum Scope { Denom(String), AssetGroup(String), diff --git a/contracts/transmuter/src/swap.rs b/contracts/transmuter/src/swap.rs index aa818c8..e8b6e5d 100644 --- a/contracts/transmuter/src/swap.rs +++ b/contracts/transmuter/src/swap.rs @@ -1,9 +1,9 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - ensure, ensure_eq, to_json_binary, Addr, BankMsg, Coin, Decimal, Deps, DepsMut, Env, Response, - StdError, Storage, Uint128, + ensure, ensure_eq, to_json_binary, Addr, BankMsg, Coin, Decimal, Deps, DepsMut, Env, Order, + Response, StdError, Storage, Uint128, }; use osmosis_std::types::osmosis::tokenfactory::v1beta1::{MsgBurn, MsgMint}; use serde::Serialize; @@ -131,9 +131,15 @@ impl Transmuter<'_> { // check and update limiters only if pool assets are not zero if let Some(updated_weights) = pool.weights()? { + let scope_value_pairs = construct_scope_value_pairs( + prev_weights, + updated_weights, + self.get_asset_group(deps.storage)?, + )?; + self.limiters.check_limits_and_update( deps.storage, - pair_weights_by_denom(prev_weights, updated_weights), + scope_value_pairs, env.block.time, )?; } @@ -307,9 +313,15 @@ impl Transmuter<'_> { // check and update limiters only if pool assets are not zero if let Some(updated_weights) = pool.weights()? { + let scope_value_pairs = construct_scope_value_pairs( + prev_weights, + updated_weights, + self.get_asset_group(deps.storage)?, + )?; + self.limiters.check_limits_and_update( deps.storage, - pair_weights_by_denom(prev_weights, updated_weights), + scope_value_pairs, env.block.time, )?; } @@ -366,9 +378,15 @@ impl Transmuter<'_> { // check and update limiters only if pool assets are not zero if let Some(updated_weights) = pool.weights()? { + let scope_value_pairs = construct_scope_value_pairs( + prev_weights, + updated_weights, + self.get_asset_group(deps.storage)?, + )?; + self.limiters.check_limits_and_update( deps.storage, - pair_weights_by_denom(prev_weights, updated_weights), + scope_value_pairs, env.block.time, )?; } @@ -421,9 +439,14 @@ impl Transmuter<'_> { // check and update limiters only if pool assets are not zero if let Some(updated_weights) = pool.weights()? { + let scope_value_pairs = construct_scope_value_pairs( + prev_weights, + updated_weights, + self.get_asset_group(deps.storage)?, + )?; self.limiters.check_limits_and_update( deps.storage, - pair_weights_by_denom(prev_weights, updated_weights), + scope_value_pairs, env.block.time, )?; } @@ -604,21 +627,62 @@ impl Transmuter<'_> { Ok(()) } + + /// get asset group mapping from storage + fn get_asset_group( + &self, + storage: &dyn Storage, + ) -> Result>, StdError> { + self.asset_group + .range(storage, None, None, Order::Ascending) + .collect::>, _>>() + } } -// TODO: compute weight pairs by target > denom -fn pair_weights_by_denom( +fn construct_scope_value_pairs( prev_weights: BTreeMap, updated_weights: Vec<(String, Decimal)>, -) -> Vec<(Scope, (Decimal, Decimal))> { - let mut denom_weight_pairs = Vec::new(); + asset_group: HashMap>, +) -> Result, StdError> { + let mut denom_weight_pairs: HashMap = HashMap::new(); + let mut asset_group_weight_pairs: HashMap = HashMap::new(); + + // Reverse index the asset groups + // TODO: handle cases where asset group contains denom that does not exist + let mut asset_groups_of_denom = HashMap::new(); + for (group, denoms) in asset_group { + for denom in denoms { + asset_groups_of_denom + .entry(denom) + .or_insert_with(Vec::new) + .push(group.clone()); + } + } - for (denom, weight) in updated_weights { - let prev_weight = prev_weights.get(denom.as_str()).unwrap_or(&weight); - denom_weight_pairs.push((Scope::denom(&denom), (*prev_weight, weight))); + for (denom, weight) in &updated_weights { + let prev_weight = prev_weights.get(denom.as_str()).unwrap_or(weight); + denom_weight_pairs.insert(Scope::denom(denom), (*prev_weight, *weight)); + + for group in asset_groups_of_denom.get(denom.as_str()).unwrap_or(&vec![]) { + match asset_group_weight_pairs.get_mut(&Scope::asset_group(group)) { + Some((prev, curr)) => { + *prev = prev.checked_add(*prev_weight)?; + *curr = curr.checked_add(*weight)?; + } + None => { + asset_group_weight_pairs + .insert(Scope::asset_group(group), (*prev_weight, *weight)); + } + } + + // TODO: check for invalid cases like total weight is not 1, proptest it + } } - denom_weight_pairs + Ok(denom_weight_pairs + .into_iter() + .chain(asset_group_weight_pairs.into_iter()) + .collect()) } /// Possible variants of swap, depending on the input and output tokens @@ -1617,4 +1681,82 @@ mod tests { assert_eq!(res, expected_res); } + + #[rstest] + #[case::empty( + HashMap::from([]), + vec![], + vec![], + )] + #[case::no_asset_group( + HashMap::from([]), + vec![ + ("eth.axl", (Decimal::percent(20), Decimal::percent(40))), + ("eth.wh", (Decimal::percent(60), Decimal::percent(40))), + ("wsteth.axl", (Decimal::percent(20), Decimal::percent(20))), + ], + vec![], + )] + #[case( + HashMap::from([ + ("axelar", vec!["eth.axl", "wsteth.axl"]), + ("wormhole", vec!["eth.wh"]), + ]), + vec![ + ("eth.axl", (Decimal::percent(20), Decimal::percent(40))), + ("wsteth.axl", (Decimal::percent(20), Decimal::percent(20))), + ("eth.wh", (Decimal::percent(60), Decimal::percent(40))), + ], + vec![ + (Scope::asset_group("axelar"), (Decimal::percent(40), Decimal::percent(60))), + (Scope::asset_group("wormhole"), (Decimal::percent(60), Decimal::percent(40))), + ], + )] + fn test_construct_scope_value_pairs( + #[case] asset_groups: HashMap<&str, Vec<&str>>, + #[case] denom_weights: Vec<(&str, (Decimal, Decimal))>, + #[case] expected_asset_group_scopes: Vec<(Scope, (Decimal, Decimal))>, + ) { + let asset_groups = asset_groups + .into_iter() + .map(|(label, asset_group)| { + ( + label.to_string(), + asset_group + .into_iter() + .map(|asset| asset.to_string()) + .collect_vec(), + ) + }) + .collect(); + + let prev_weights = denom_weights + .clone() + .into_iter() + .map(|(denom, (prev_weight, _))| (denom.to_string(), prev_weight)) + .collect(); + + let updated_weights = denom_weights + .clone() + .into_iter() + .map(|(denom, (_, updated_weight))| (denom.to_string(), updated_weight)) + .collect_vec(); + + let mut scope_value_pairs = + construct_scope_value_pairs(prev_weights, updated_weights, asset_groups).unwrap(); + + let scope_denom_value_pairs = denom_weights + .into_iter() + .map(|(denom, weight_transition)| (Scope::denom(denom), weight_transition)) + .collect_vec(); + + let mut expected_scope_value_pairs = + vec![scope_denom_value_pairs, expected_asset_group_scopes].concat(); + + // assert by disregrard order + scope_value_pairs.sort_by_key(|(scope, _)| scope.key()); + expected_scope_value_pairs.sort_by_key(|(scope, _)| scope.key()); + + assert_eq!(scope_value_pairs, expected_scope_value_pairs); + } } From fd705cc6ede877489a405099f5581b320be52280 Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Mon, 30 Sep 2024 15:46:35 +0700 Subject: [PATCH 03/26] Add and remove asset group properly --- contracts/transmuter/src/contract.rs | 354 ++++++++++++++++++++++++++- contracts/transmuter/src/limiter.rs | 69 +++++- contracts/transmuter/src/swap.rs | 5 + 3 files changed, 419 insertions(+), 9 deletions(-) diff --git a/contracts/transmuter/src/contract.rs b/contracts/transmuter/src/contract.rs index 0828a67..0b18145 100644 --- a/contracts/transmuter/src/contract.rs +++ b/contracts/transmuter/src/contract.rs @@ -14,8 +14,8 @@ use crate::{ }; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - ensure, ensure_ne, Addr, Coin, Decimal, DepsMut, Env, Reply, Response, StdError, Storage, - SubMsg, Uint128, + ensure, ensure_ne, Addr, Coin, Decimal, DepsMut, Env, Order, Reply, Response, StdError, + Storage, SubMsg, Uint128, }; use cw_storage_plus::{Item, Map}; @@ -302,12 +302,22 @@ impl Transmuter<'_> { // remove asset group self.asset_group.remove(deps.storage, &label); - // remove all limiter for asset group - todo!("remove all limiter for asset group"); + // remove all limiters for asset group + let limiters = self + .limiters + .list_limiters_by_scope(deps.storage, &Scope::AssetGroup(label.clone()))?; + + for (limiter_label, _) in limiters { + self.limiters.unchecked_deregister( + deps.storage, + Scope::AssetGroup(label.clone()), + &limiter_label, + )?; + } - // Ok(Response::new() - // .add_attribute("method", "remove_asset_group") - // .add_attribute("label", label)) + Ok(Response::new() + .add_attribute("method", "remove_asset_group") + .add_attribute("label", label)) } /// Mark designated denoms as corrupted assets. @@ -649,6 +659,19 @@ impl Transmuter<'_> { Ok(ListLimitersResponse { limiters }) } + #[sv::msg(query)] + fn list_asset_groups( + &self, + QueryCtx { deps, env: _ }: QueryCtx, + ) -> Result { + let asset_groups = self + .asset_group + .range(deps.storage, None, None, Order::Ascending) + .collect::, _>>()?; + + Ok(ListAssetGroupsResponse { asset_groups }) + } + #[sv::msg(query)] pub fn get_shares( &self, @@ -916,6 +939,11 @@ pub struct ListLimitersResponse { pub limiters: Vec<((String, String), Limiter)>, } +#[cw_serde] +pub struct ListAssetGroupsResponse { + pub asset_groups: BTreeMap>, +} + #[cw_serde] pub struct GetSharesResponse { pub shares: Uint128, @@ -4006,4 +4034,316 @@ mod tests { }) ); } + + #[test] + fn test_asset_group() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let admin = "admin"; + + // Setup balance for each asset + deps.querier.update_balance( + env.contract.address.clone(), + vec![ + Coin::new(1000000, "asset1"), + Coin::new(1000000, "asset2"), + Coin::new(1000000, "asset3"), + ], + ); + + // Initialize the contract + let instantiate_msg = InstantiateMsg { + admin: Some(admin.to_string()), + moderator: "moderator".to_string(), + pool_asset_configs: vec![ + AssetConfig { + denom: "asset1".to_string(), + normalization_factor: Uint128::from(1000000u128), + }, + AssetConfig { + denom: "asset2".to_string(), + normalization_factor: Uint128::from(1000000u128), + }, + AssetConfig { + denom: "asset3".to_string(), + normalization_factor: Uint128::from(1000000u128), + }, + ], + alloyed_asset_subdenom: "alloyed".to_string(), + alloyed_asset_normalization_factor: Uint128::from(1000000u128), + }; + + let info = mock_info(admin, &[]); + instantiate(deps.as_mut(), env.clone(), info.clone(), instantiate_msg).unwrap(); + + // Create asset group + let create_asset_group_msg = ContractExecMsg::Transmuter(ExecMsg::CreateAssetGroup { + label: "group1".to_string(), + denoms: vec!["asset1".to_string(), "asset2".to_string()], + }); + + // Test non-admin trying to create asset group + let non_admin_info = mock_info("non_admin", &[]); + let non_admin_create_msg = ContractExecMsg::Transmuter(ExecMsg::CreateAssetGroup { + label: "group1".to_string(), + denoms: vec!["asset1".to_string(), "asset2".to_string()], + }); + let err = execute( + deps.as_mut(), + env.clone(), + non_admin_info, + non_admin_create_msg, + ) + .unwrap_err(); + assert_eq!(err, ContractError::Unauthorized {}); + + // Test admin creating asset group + let res = execute(deps.as_mut(), env.clone(), info, create_asset_group_msg).unwrap(); + + assert_eq!( + res.attributes, + vec![ + attr("method", "create_asset_group"), + attr("label", "group1"), + ] + ); + + // List asset groups + let list_asset_groups_msg = ContractQueryMsg::Transmuter(QueryMsg::ListAssetGroups {}); + let list_asset_groups_res: Result = + query(deps.as_ref(), env.clone(), list_asset_groups_msg) + .map(|value| from_json(value).unwrap()); + + assert_eq!( + list_asset_groups_res, + Ok(ListAssetGroupsResponse { + asset_groups: BTreeMap::from([( + "group1".to_string(), + vec!["asset1".to_string(), "asset2".to_string()], + )]), + }) + ); + + // Try setting limiter with non-existent group + let register_limiter_msg = ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { + label: "limiter1".to_string(), + scope: Scope::asset_group("group2"), + limiter_params: LimiterParams::ChangeLimiter { + window_config: WindowConfig { + window_size: 86400u64.into(), + division_count: 10u64.into(), + }, + boundary_offset: Decimal::percent(10), + }, + }); + + let register_limiter_info = mock_info("admin", &[]); + let err = execute( + deps.as_mut(), + env.clone(), + register_limiter_info.clone(), + register_limiter_msg.clone(), + ) + .unwrap_err(); + + assert!(matches!(err, ContractError::AssetGroupNotFound { .. })); + + // Create group2 + let create_asset_group_msg2 = ContractExecMsg::Transmuter(ExecMsg::CreateAssetGroup { + label: "group2".to_string(), + denoms: vec!["asset3".to_string()], + }); + + let create_asset_group_info2 = mock_info("admin", &[]); + let res2 = execute( + deps.as_mut(), + env.clone(), + create_asset_group_info2, + create_asset_group_msg2, + ) + .unwrap(); + + assert_eq!( + res2.attributes, + vec![ + attr("method", "create_asset_group"), + attr("label", "group2"), + ] + ); + + // Verify group2 was created + let list_asset_groups_msg2 = ContractQueryMsg::Transmuter(QueryMsg::ListAssetGroups {}); + let list_asset_groups_res2: Result = + query(deps.as_ref(), env.clone(), list_asset_groups_msg2) + .map(|value| from_json(value).unwrap()); + + assert_eq!( + list_asset_groups_res2, + Ok(ListAssetGroupsResponse { + asset_groups: BTreeMap::from([ + ( + "group1".to_string(), + vec!["asset1".to_string(), "asset2".to_string()] + ), + ("group2".to_string(), vec!["asset3".to_string()]), + ]), + }) + ); + + // Try to register limiter for group2 + let res3 = execute( + deps.as_mut(), + env.clone(), + register_limiter_info, + register_limiter_msg, + ) + .unwrap(); + + assert_eq!( + res3.attributes, + vec![ + attr("method", "register_limiter"), + attr("label", "limiter1"), + attr("scope", "asset_group::group2"), + attr("limiter_type", "change_limiter"), + attr("window_size", "86400"), + attr("division_count", "10"), + attr("boundary_offset", "0.1"), + ] + ); + + // Verify limiter was registered + let list_limiters_msg = ContractQueryMsg::Transmuter(QueryMsg::ListLimiters {}); + let list_limiters_res: ListLimitersResponse = + from_json(query(deps.as_ref(), env.clone(), list_limiters_msg).unwrap()).unwrap(); + + assert_eq!( + list_limiters_res.limiters, + vec![( + ( + Scope::asset_group("group2").to_string(), + "limiter1".to_string() + ), + Limiter::ChangeLimiter( + ChangeLimiter::new( + WindowConfig { + window_size: 86400u64.into(), + division_count: 10u64.into(), + }, + Decimal::percent(10), + ) + .unwrap() + ) + )] + ); + + // Try to create a group with a non-existing asset + let create_invalid_group_msg = ContractExecMsg::Transmuter(ExecMsg::CreateAssetGroup { + label: "invalid_group".to_string(), + denoms: vec!["asset1".to_string(), "non_existing_asset".to_string()], + }); + + let admin_info = mock_info(admin, &[]); + let err = execute( + deps.as_mut(), + env.clone(), + admin_info, + create_invalid_group_msg, + ) + .unwrap_err(); + + assert_eq!( + err, + ContractError::InvalidPoolAssetDenom { + denom: "non_existing_asset".to_string() + } + ); + + // Verify that the invalid group was not created + let list_asset_groups_msg = ContractQueryMsg::Transmuter(QueryMsg::ListAssetGroups {}); + let list_asset_groups_res: ListAssetGroupsResponse = + from_json(query(deps.as_ref(), env.clone(), list_asset_groups_msg).unwrap()).unwrap(); + + assert_eq!( + list_asset_groups_res.asset_groups, + BTreeMap::from([ + ( + "group1".to_string(), + vec!["asset1".to_string(), "asset2".to_string()] + ), + ("group2".to_string(), vec!["asset3".to_string()]), + ]) + ); + + // Test removing an asset group + let remove_group_msg = ContractExecMsg::Transmuter(ExecMsg::RemoveAssetGroup { + label: "group2".to_string(), + }); + + // Try to remove the group with a non-admin account + let non_admin_info = mock_info("non_admin", &[]); + let err = execute( + deps.as_mut(), + env.clone(), + non_admin_info, + remove_group_msg.clone(), + ) + .unwrap_err(); + + assert_eq!(err, ContractError::Unauthorized {}); + + // Remove the group with the admin account + let admin_info = mock_info(admin, &[]); + let res = execute(deps.as_mut(), env.clone(), admin_info, remove_group_msg).unwrap(); + + assert_eq!( + res.attributes, + vec![ + attr("method", "remove_asset_group"), + attr("label", "group2"), + ] + ); + + // Verify that the group was removed + let list_asset_groups_msg = ContractQueryMsg::Transmuter(QueryMsg::ListAssetGroups {}); + let list_asset_groups_res: ListAssetGroupsResponse = + from_json(query(deps.as_ref(), env.clone(), list_asset_groups_msg).unwrap()).unwrap(); + + assert_eq!( + list_asset_groups_res.asset_groups, + BTreeMap::from([( + "group1".to_string(), + vec!["asset1".to_string(), "asset2".to_string()] + )]) + ); + + // Test that limiter1 is removed along with the asset group + let list_limiters_msg = ContractQueryMsg::Transmuter(QueryMsg::ListLimiters {}); + let list_limiters_res: ListLimitersResponse = + from_json(query(deps.as_ref(), env.clone(), list_limiters_msg).unwrap()).unwrap(); + + // Check that limiter1 is not in the list of limiters + assert_eq!(list_limiters_res.limiters, vec![]); + + // Test removing a non-existing asset group + let remove_nonexistent_group_msg = ContractExecMsg::Transmuter(ExecMsg::RemoveAssetGroup { + label: "non_existent_group".to_string(), + }); + + let admin_info = mock_info(admin, &[]); + let err = execute( + deps.as_mut(), + env.clone(), + admin_info, + remove_nonexistent_group_msg, + ) + .unwrap_err(); + + assert_eq!( + err, + ContractError::AssetGroupNotFound { + label: "non_existent_group".to_string() + } + ); + } } diff --git a/contracts/transmuter/src/limiter.rs b/contracts/transmuter/src/limiter.rs index 8271bf8..c1dd7d8 100644 --- a/contracts/transmuter/src/limiter.rs +++ b/contracts/transmuter/src/limiter.rs @@ -395,6 +395,27 @@ impl<'a> Limiters<'a> { Ok(()) } + /// Deregister a limiter without checking if it will be empty. + /// This is useful when the scope is being removed, so that limiters for the scope are no longer needed. + pub fn unchecked_deregister( + &self, + storage: &mut dyn Storage, + scope: Scope, + label: &str, + ) -> Result { + let scope_key = scope.key(); + match self.limiters.may_load(storage, (&scope_key, label))? { + Some(limiter) => { + self.limiters.remove(storage, (&scope_key, label)); + Ok(limiter) + } + None => Err(ContractError::LimiterDoesNotExist { + scope, + label: label.to_string(), + }), + } + } + pub fn deregister( &self, storage: &mut dyn Storage, @@ -404,11 +425,11 @@ impl<'a> Limiters<'a> { let scope_key = scope.key(); match self.limiters.may_load(storage, (&scope_key, label))? { Some(limiter) => { - let limiter_for_denom_will_not_be_empty = + let limiter_for_scope_will_not_be_empty = self.list_limiters_by_scope(storage, &scope)?.len() >= 2; ensure!( - limiter_for_denom_will_not_be_empty, + limiter_for_scope_will_not_be_empty, ContractError::EmptyLimiterNotAllowed { scope } ); @@ -1241,6 +1262,50 @@ mod tests { ] ); } + + #[test] + fn test_unchecked_deregister() { + let mut deps = mock_dependencies(); + let limiter = Limiters::new("limiters"); + + // Register two limiters for denoma and one for denomb + limiter + .register( + &mut deps.storage, + Scope::denom("denoma"), + "1h", + LimiterParams::ChangeLimiter { + window_config: WindowConfig { + window_size: Uint64::from(3_600_000_000_000u64), + division_count: Uint64::from(2u64), + }, + boundary_offset: Decimal::percent(10), + }, + ) + .unwrap(); + + // Unchecked deregister one limiter from denoma + let removed_limiter = limiter + .unchecked_deregister(&mut deps.storage, Scope::denom("denoma"), "1h") + .unwrap(); + + // Check that the removed limiter is correct + assert_eq!( + removed_limiter, + Limiter::ChangeLimiter(ChangeLimiter { + divisions: vec![], + latest_value: Decimal::zero(), + window_config: WindowConfig { + window_size: Uint64::from(3_600_000_000_000u64), + division_count: Uint64::from(2u64), + }, + boundary_offset: Decimal::percent(10) + }) + ); + + // Check that the remaining limiters are correct + assert_eq!(limiter.list_limiters(&deps.storage).unwrap(), vec![]); + } } mod set_config { diff --git a/contracts/transmuter/src/swap.rs b/contracts/transmuter/src/swap.rs index e8b6e5d..a28d505 100644 --- a/contracts/transmuter/src/swap.rs +++ b/contracts/transmuter/src/swap.rs @@ -621,6 +621,7 @@ impl Transmuter<'_> { Scope::denom(corrupted.denom()), // TODO: bubble this up )?; + // TODO: remove limiters from asset group too // TODO: remove denom from asset group, if asset group is empty, remove it } } @@ -1760,3 +1761,7 @@ mod tests { assert_eq!(scope_value_pairs, expected_scope_value_pairs); } } + +// TODO: +// - integration tests for asset group limiters +// - migration: prefixing exsting limiters key with "denom::" From 7a3a047364bfe2cd9b908aea21a770ba77cb2c73 Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Tue, 1 Oct 2024 15:47:50 +0700 Subject: [PATCH 04/26] create asset group --- contracts/transmuter/src/asset.rs | 32 +- contracts/transmuter/src/asset_group.rs | 193 +++++++++ contracts/transmuter/src/contract.rs | 399 ++++++++++++++---- contracts/transmuter/src/corruptable.rs | 5 + contracts/transmuter/src/error.rs | 4 +- contracts/transmuter/src/lib.rs | 2 + contracts/transmuter/src/swap.rs | 56 ++- .../src/transmuter_pool/corrupted_assets.rs | 91 ++-- 8 files changed, 595 insertions(+), 187 deletions(-) create mode 100644 contracts/transmuter/src/asset_group.rs create mode 100644 contracts/transmuter/src/corruptable.rs diff --git a/contracts/transmuter/src/asset.rs b/contracts/transmuter/src/asset.rs index 4337989..9d0db46 100644 --- a/contracts/transmuter/src/asset.rs +++ b/contracts/transmuter/src/asset.rs @@ -1,7 +1,7 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{ensure, Coin, Deps, StdError, Uint128, Uint256}; -use crate::ContractError; +use crate::{corruptable::Corruptable, ContractError}; #[derive(PartialEq)] pub enum Rounding { @@ -121,16 +121,6 @@ impl Asset { Ok(self) } - pub fn mark_as_corrupted(&'_ mut self) -> &'_ Self { - self.is_corrupted = true; - self - } - - pub fn unmark_as_corrupted(&'_ mut self) -> &'_ Self { - self.is_corrupted = false; - self - } - pub fn denom(&self) -> &str { &self.denom } @@ -143,10 +133,6 @@ impl Asset { self.normalization_factor } - pub fn is_corrupted(&self) -> bool { - self.is_corrupted - } - pub fn config(&self) -> AssetConfig { AssetConfig { denom: self.denom.clone(), @@ -188,6 +174,22 @@ impl Asset { } } +impl Corruptable for Asset { + fn is_corrupted(&self) -> bool { + self.is_corrupted + } + + fn mark_as_corrupted(&mut self) -> &mut Self { + self.is_corrupted = true; + self + } + + fn unmark_as_corrupted(&mut self) -> &mut Self { + self.is_corrupted = false; + self + } +} + /// Convert amount to target asset's amount with the same value /// /// target_amount / target_normalization_factor = amount / source_normalization_factor diff --git a/contracts/transmuter/src/asset_group.rs b/contracts/transmuter/src/asset_group.rs new file mode 100644 index 0000000..198480a --- /dev/null +++ b/contracts/transmuter/src/asset_group.rs @@ -0,0 +1,193 @@ +use std::collections::BTreeMap; + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::ensure; + +use crate::{corruptable::Corruptable, ContractError}; + +#[cw_serde] +pub struct AssetGroup { + denoms: Vec, + is_corrupted: bool, +} + +impl AssetGroup { + pub fn new(denoms: Vec) -> Self { + Self { + denoms, + is_corrupted: false, + } + } + + pub fn denoms(&self) -> &[String] { + &self.denoms + } + + pub fn into_denoms(self) -> Vec { + self.denoms + } + + pub fn add_denoms(&mut self, denoms: Vec) -> &mut Self { + self.denoms.extend(denoms); + self + } + + pub fn remove_denoms(&mut self, denoms: Vec) -> &mut Self { + self.denoms.retain(|d| !denoms.contains(d)); + self + } +} + +impl Corruptable for AssetGroup { + fn is_corrupted(&self) -> bool { + self.is_corrupted + } + + fn mark_as_corrupted(&mut self) -> &mut Self { + self.is_corrupted = true; + self + } + + fn unmark_as_corrupted(&mut self) -> &mut Self { + self.is_corrupted = false; + self + } +} + +#[cw_serde] +pub struct AssetGroups(BTreeMap); + +impl Default for AssetGroups { + fn default() -> Self { + Self::new() + } +} + +impl AssetGroups { + pub fn new() -> Self { + Self(BTreeMap::new()) + } + + pub fn has(&self, label: &str) -> bool { + self.0.contains_key(label) + } + + pub fn mark_corrupted_asset_group(&mut self, label: &str) -> Result<&mut Self, ContractError> { + let Self(asset_groups) = self; + + asset_groups + .get_mut(label) + .ok_or_else(|| ContractError::AssetGroupNotFound { + label: label.to_string(), + })? + .mark_as_corrupted(); + + Ok(self) + } + + pub fn unmark_corrupted_asset_group( + &mut self, + label: &str, + ) -> Result<&mut Self, ContractError> { + let Self(asset_groups) = self; + + asset_groups + .get_mut(label) + .ok_or_else(|| ContractError::AssetGroupNotFound { + label: label.to_string(), + })? + .unmark_as_corrupted(); + + Ok(self) + } + + pub fn create_asset_group( + &mut self, + label: String, + denoms: Vec, + ) -> Result<&mut Self, ContractError> { + let Self(asset_groups) = self; + + ensure!( + !asset_groups.contains_key(&label), + ContractError::AssetGroupAlreadyExists { + label: label.clone() + } + ); + + asset_groups.insert(label, AssetGroup::new(denoms)); + + Ok(self) + } + + pub fn remove_asset_group(&mut self, label: &str) -> Result<&mut Self, ContractError> { + let Self(asset_groups) = self; + + ensure!( + asset_groups.remove(label).is_some(), + ContractError::AssetGroupNotFound { + label: label.to_string() + } + ); + + Ok(self) + } + + pub fn into_inner(self) -> BTreeMap { + self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_add_remove_denoms() { + let mut group = AssetGroup::new(vec!["denom1".to_string(), "denom2".to_string()]); + + // Test initial state + assert_eq!(group.denoms(), &["denom1", "denom2"]); + + // Test adding denoms + group.add_denoms(vec!["denom3".to_string(), "denom4".to_string()]); + assert_eq!(group.denoms(), &["denom1", "denom2", "denom3", "denom4"]); + + // Test adding duplicate denom + group.add_denoms(vec!["denom2".to_string(), "denom5".to_string()]); + assert_eq!( + group.denoms(), + &["denom1", "denom2", "denom3", "denom4", "denom2", "denom5"] + ); + + // Test removing denoms + group.remove_denoms(vec!["denom2".to_string(), "denom4".to_string()]); + assert_eq!(group.denoms(), &["denom1", "denom3", "denom5"]); + + // Test removing non-existent denom + group.remove_denoms(vec!["denom6".to_string()]); + assert_eq!(group.denoms(), &["denom1", "denom3", "denom5"]); + } + + #[test] + fn test_mark_unmark_corrupted() { + let mut group = AssetGroup::new(vec!["denom1".to_string(), "denom2".to_string()]); + + // Test initial state + assert!(!group.is_corrupted()); + + // Test marking as corrupted + group.mark_as_corrupted(); + assert!(group.is_corrupted()); + + // Test unmarking as corrupted + group.unmark_as_corrupted(); + assert!(!group.is_corrupted()); + + // Test marking and unmarking multiple times + group.mark_as_corrupted().mark_as_corrupted(); + assert!(group.is_corrupted()); + group.unmark_as_corrupted().unmark_as_corrupted(); + assert!(!group.is_corrupted()); + } +} diff --git a/contracts/transmuter/src/contract.rs b/contracts/transmuter/src/contract.rs index 0b18145..aba383a 100644 --- a/contracts/transmuter/src/contract.rs +++ b/contracts/transmuter/src/contract.rs @@ -1,4 +1,8 @@ -use crate::scope::Scope; +use crate::{ + asset_group::{AssetGroup, AssetGroups}, + corruptable::Corruptable, + scope::Scope, +}; use std::{collections::BTreeMap, iter}; use crate::{ @@ -14,11 +18,11 @@ use crate::{ }; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - ensure, ensure_ne, Addr, Coin, Decimal, DepsMut, Env, Order, Reply, Response, StdError, - Storage, SubMsg, Uint128, + ensure, ensure_ne, Addr, Coin, Decimal, DepsMut, Env, Reply, Response, StdError, Storage, + SubMsg, Uint128, }; -use cw_storage_plus::{Item, Map}; +use cw_storage_plus::Item; use osmosis_std::types::{ cosmos::bank::v1beta1::Metadata, osmosis::tokenfactory::v1beta1::{MsgCreateDenom, MsgCreateDenomResponse, MsgSetDenomMetadata}, @@ -38,16 +42,13 @@ const CREATE_ALLOYED_DENOM_REPLY_ID: u64 = 1; /// Prefix for alloyed asset denom const ALLOYED_PREFIX: &str = "alloyed"; -// asset_group: label -> denoms -pub type AssetGroup<'a> = Map<'a, &'a str, Vec>; - pub struct Transmuter<'a> { pub(crate) active_status: Item<'a, bool>, pub(crate) pool: Item<'a, TransmuterPool>, pub(crate) alloyed_asset: AlloyedAsset<'a>, pub(crate) role: Role<'a>, pub(crate) limiters: Limiters<'a>, - pub(crate) asset_group: AssetGroup<'a>, + pub(crate) asset_groups: Item<'a, AssetGroups>, } pub mod key { @@ -75,7 +76,7 @@ impl Transmuter<'_> { ), role: Role::new(key::ADMIN, key::MODERATOR), limiters: Limiters::new(key::LIMITERS), - asset_group: AssetGroup::new(key::ASSET_GROUP), + asset_groups: Item::new(key::ASSET_GROUP), } } @@ -140,6 +141,9 @@ impl Transmuter<'_> { self.alloyed_asset .set_normalization_factor(deps.storage, alloyed_asset_normalization_factor)?; + // initialize asset groups + self.asset_groups.save(deps.storage, &AssetGroups::new())?; + Ok(Response::new() .add_attribute("method", "instantiate") .add_attribute("contract_name", CONTRACT_NAME) @@ -268,14 +272,14 @@ impl Transmuter<'_> { ); } - // ensure that asset group does not exist - ensure!( - self.asset_group.may_load(deps.storage, &label)?.is_none(), - ContractError::AssetGroupAlreadyExists { label } - ); + // create asset group + let mut asset_groups = self + .asset_groups + .may_load(deps.storage)? + .unwrap_or_default(); - // save asset group - self.asset_group.save(deps.storage, &label, &denoms)?; + asset_groups.create_asset_group(label.clone(), denoms)?; + self.asset_groups.save(deps.storage, &asset_groups)?; Ok(Response::new() .add_attribute("method", "create_asset_group") @@ -293,14 +297,13 @@ impl Transmuter<'_> { // only admin can remove asset group ensure_admin_authority!(info.sender, self.role.admin, deps.as_ref()); - // check if asset group exists - ensure!( - self.asset_group.load(deps.storage, &label).is_ok(), - ContractError::AssetGroupNotFound { label } - ); + let mut asset_groups = self + .asset_groups + .may_load(deps.storage)? + .unwrap_or_default(); - // remove asset group - self.asset_group.remove(deps.storage, &label); + asset_groups.remove_asset_group(&label)?; + self.asset_groups.save(deps.storage, &asset_groups)?; // remove all limiters for asset group let limiters = self @@ -320,51 +323,80 @@ impl Transmuter<'_> { .add_attribute("label", label)) } - /// Mark designated denoms as corrupted assets. - /// As a result, the corrupted assets will not allowed to be increased by any means, + /// Mark designated scopes as corrupted scopes. + /// As a result, the corrupted scopes will not allowed to be increased by any means, /// both in terms of amount and weight. - /// The only way to redeem other pool asset, is to also redeem the corrupted asset + /// The only way to redeem other pool asset outside of the corrupted scopes is + /// to also redeem asset within the corrupted scopes /// with the same pool-defnined value. #[sv::msg(exec)] - fn mark_corrupted_assets( + fn mark_corrupted_scopes( &self, ExecCtx { deps, env: _, info }: ExecCtx, - denoms: Vec, + scopes: Vec, ) -> Result { - non_empty_input_required("denoms", &denoms)?; + non_empty_input_required("scopes", &scopes)?; nonpayable(&info.funds)?; // only moderator can mark corrupted assets ensure_moderator_authority!(info.sender, self.role.moderator, deps.as_ref()); - self.pool - .update(deps.storage, |mut pool| -> Result<_, ContractError> { - pool.mark_corrupted_assets(&denoms)?; - Ok(pool) - })?; + let mut pool = self.pool.load(deps.storage)?; + let mut asset_groups = self + .asset_groups + .may_load(deps.storage)? + .unwrap_or_default(); + + for scope in &scopes { + match scope { + Scope::Denom(denom) => { + pool.mark_corrupted_asset(denom)?; + } + Scope::AssetGroup(label) => { + asset_groups.mark_corrupted_asset_group(label)?; + } + } + } + + self.pool.save(deps.storage, &pool)?; + self.asset_groups.save(deps.storage, &asset_groups)?; - Ok(Response::new().add_attribute("method", "mark_corrupted_assets")) + Ok(Response::new().add_attribute("method", "mark_corrupted_scopes")) } #[sv::msg(exec)] - fn unmark_corrupted_assets( + fn unmark_corrupted_scopes( &self, ExecCtx { deps, env: _, info }: ExecCtx, - denoms: Vec, + scopes: Vec, ) -> Result { - non_empty_input_required("denoms", &denoms)?; + non_empty_input_required("scopes", &scopes)?; nonpayable(&info.funds)?; // only moderator can unmark corrupted assets ensure_moderator_authority!(info.sender, self.role.moderator, deps.as_ref()); - self.pool - .update(deps.storage, |mut pool| -> Result<_, ContractError> { - pool.unmark_corrupted_assets(&denoms)?; - Ok(pool) - })?; + let mut pool = self.pool.load(deps.storage)?; + let mut asset_groups = self + .asset_groups + .may_load(deps.storage)? + .unwrap_or_default(); + + for scope in &scopes { + match scope { + Scope::Denom(denom) => { + pool.unmark_corrupted_asset(denom)?; + } + Scope::AssetGroup(label) => { + asset_groups.unmark_corrupted_asset_group(label)?; + } + } + } - Ok(Response::new().add_attribute("method", "unmark_corrupted_assets")) + self.pool.save(deps.storage, &pool)?; + self.asset_groups.save(deps.storage, &asset_groups)?; + + Ok(Response::new().add_attribute("method", "unmark_corrupted_scopes")) } #[sv::msg(exec)] @@ -380,6 +412,11 @@ impl Transmuter<'_> { // only admin can register limiter ensure_admin_authority!(info.sender, self.role.admin, deps.as_ref()); + let asset_groups = self + .asset_groups + .may_load(deps.storage)? + .unwrap_or_default(); + match scope.clone() { Scope::Denom(denom) => { // ensure pool has the specified denom @@ -391,7 +428,7 @@ impl Transmuter<'_> { Scope::AssetGroup(label) => { // check if asset group exists ensure!( - self.asset_group.may_load(deps.storage, &label)?.is_some(), + asset_groups.has(&label), ContractError::AssetGroupNotFound { label } ); } @@ -665,11 +702,13 @@ impl Transmuter<'_> { QueryCtx { deps, env: _ }: QueryCtx, ) -> Result { let asset_groups = self - .asset_group - .range(deps.storage, None, None, Order::Ascending) - .collect::, _>>()?; + .asset_groups + .may_load(deps.storage)? + .unwrap_or_default(); - Ok(ListAssetGroupsResponse { asset_groups }) + Ok(ListAssetGroupsResponse { + asset_groups: asset_groups.into_inner(), + }) } #[sv::msg(query)] @@ -816,18 +855,30 @@ impl Transmuter<'_> { } #[sv::msg(query)] - pub(crate) fn get_corrupted_denoms( + pub(crate) fn get_corrupted_scopes( &self, QueryCtx { deps, env: _ }: QueryCtx, - ) -> Result { + ) -> Result { let pool = self.pool.load(deps.storage)?; - let corrupted_denoms = pool + let asset_groups = self + .asset_groups + .may_load(deps.storage)? + .unwrap_or_default(); + + let corrupted_assets = pool .corrupted_assets() .into_iter() - .map(|a| a.denom().to_string()) - .collect(); + .map(|a| Scope::denom(a.denom())); + + let corrupted_asset_groups = asset_groups + .into_inner() + .into_iter() + .filter(|(_, asset_group)| asset_group.is_corrupted()) + .map(|(label, _)| Scope::AssetGroup(label)); - Ok(GetCorrruptedDenomsResponse { corrupted_denoms }) + Ok(GetCorrruptedScopesResponse { + corrupted_scopes: corrupted_assets.chain(corrupted_asset_groups).collect(), + }) } // --- admin --- @@ -941,7 +992,7 @@ pub struct ListLimitersResponse { #[cw_serde] pub struct ListAssetGroupsResponse { - pub asset_groups: BTreeMap>, + pub asset_groups: BTreeMap, } #[cw_serde] @@ -990,8 +1041,8 @@ pub struct CalcInAmtGivenOutResponse { } #[cw_serde] -pub struct GetCorrruptedDenomsResponse { - pub corrupted_denoms: Vec, +pub struct GetCorrruptedScopesResponse { + pub corrupted_scopes: Vec, } #[cw_serde] @@ -1366,8 +1417,8 @@ mod tests { // Mark corrupted assets by non-moderator let info = mock_info("someone", &[]); - let mark_corrupted_assets_msg = ContractExecMsg::Transmuter(ExecMsg::MarkCorruptedAssets { - denoms: vec!["wbtc".to_string(), "tbtc".to_string()], + let mark_corrupted_assets_msg = ContractExecMsg::Transmuter(ExecMsg::MarkCorruptedScopes { + scopes: vec![Scope::denom("wbtc"), Scope::denom("tbtc")], }); let res = execute( @@ -1384,12 +1435,12 @@ mod tests { let res = query( deps.as_ref(), env.clone(), - ContractQueryMsg::Transmuter(QueryMsg::GetCorruptedDenoms {}), + ContractQueryMsg::Transmuter(QueryMsg::GetCorruptedScopes {}), ) .unwrap(); - let GetCorrruptedDenomsResponse { corrupted_denoms } = from_json(res).unwrap(); + let GetCorrruptedScopesResponse { corrupted_scopes } = from_json(res).unwrap(); - assert_eq!(corrupted_denoms, Vec::::new()); + assert_eq!(corrupted_scopes, Vec::::new()); // The asset must not yet be removed let res = query( @@ -1468,9 +1519,9 @@ mod tests { execute(deps.as_mut(), env.clone(), info.clone(), exit_pool_msg).unwrap(); // Mark corrupted assets by moderator - let corrupted_denoms = vec!["wbtc".to_string(), "tbtc".to_string()]; - let mark_corrupted_assets_msg = ContractExecMsg::Transmuter(ExecMsg::MarkCorruptedAssets { - denoms: corrupted_denoms.clone(), + let corrupted_scopes = vec![Scope::denom("wbtc"), Scope::denom("tbtc")]; + let mark_corrupted_assets_msg = ContractExecMsg::Transmuter(ExecMsg::MarkCorruptedScopes { + scopes: corrupted_scopes.clone(), }); let info = mock_info(moderator, &[]); @@ -1488,12 +1539,15 @@ mod tests { let res = query( deps.as_ref(), env.clone(), - ContractQueryMsg::Transmuter(QueryMsg::GetCorruptedDenoms {}), + ContractQueryMsg::Transmuter(QueryMsg::GetCorruptedScopes {}), ) .unwrap(); - let res: GetCorrruptedDenomsResponse = from_json(res).unwrap(); + let get_corrupted_scopes_response: GetCorrruptedScopesResponse = from_json(res).unwrap(); - assert_eq!(res.corrupted_denoms, corrupted_denoms); + assert_eq!( + get_corrupted_scopes_response.corrupted_scopes, + corrupted_scopes + ); // Check if the assets were removed let res = query( @@ -1548,9 +1602,14 @@ mod tests { let env = increase_block_height(&env, 1); - for denom in corrupted_denoms { - let expected_err = ContractError::CorruptedAssetRelativelyIncreased { - denom: denom.clone(), + for scope in corrupted_scopes { + let expected_err = ContractError::CorruptedScopeRelativelyIncreased { + scope: scope.clone(), + }; + + let denom = match scope { + Scope::Denom(denom) => denom, + _ => unreachable!(), }; // join with corrupted denom should fail @@ -1627,7 +1686,7 @@ mod tests { let err = execute(deps.as_mut(), env.clone(), info, exit_pool_msg).unwrap_err(); assert!(matches!( err, - ContractError::CorruptedAssetRelativelyIncreased { .. } + ContractError::CorruptedScopeRelativelyIncreased { .. } )); let exit_pool_msg = ContractExecMsg::Transmuter(ExecMsg::ExitPool { @@ -1643,7 +1702,7 @@ mod tests { let err = execute(deps.as_mut(), env.clone(), info, exit_pool_msg).unwrap_err(); assert!(matches!( err, - ContractError::CorruptedAssetRelativelyIncreased { .. } + ContractError::CorruptedScopeRelativelyIncreased { .. } )); } @@ -1684,8 +1743,8 @@ mod tests { assert_eq!( err, - ContractError::CorruptedAssetRelativelyIncreased { - denom: "wbtc".to_string() + ContractError::CorruptedScopeRelativelyIncreased { + scope: Scope::denom("wbtc") } ); @@ -1749,8 +1808,8 @@ mod tests { // try unmark nbtc should fail let unmark_corrupted_assets_msg = - ContractExecMsg::Transmuter(ExecMsg::UnmarkCorruptedAssets { - denoms: vec!["nbtc".to_string()], + ContractExecMsg::Transmuter(ExecMsg::UnmarkCorruptedScopes { + scopes: vec![Scope::denom("nbtc")], }); let info = mock_info(moderator, &[]); @@ -1771,8 +1830,8 @@ mod tests { // unmark tbtc by non moderator should fail let unmark_corrupted_assets_msg = - ContractExecMsg::Transmuter(ExecMsg::UnmarkCorruptedAssets { - denoms: vec!["tbtc".to_string()], + ContractExecMsg::Transmuter(ExecMsg::UnmarkCorruptedScopes { + scopes: vec![Scope::denom("tbtc")], }); let info = mock_info("someone", &[]); @@ -1788,8 +1847,8 @@ mod tests { // unmark tbtc let unmark_corrupted_assets_msg = - ContractExecMsg::Transmuter(ExecMsg::UnmarkCorruptedAssets { - denoms: vec!["tbtc".to_string()], + ContractExecMsg::Transmuter(ExecMsg::UnmarkCorruptedScopes { + scopes: vec![Scope::denom("tbtc")], }); let info = mock_info(moderator, &[]); @@ -1805,13 +1864,13 @@ mod tests { let res = query( deps.as_ref(), env.clone(), - ContractQueryMsg::Transmuter(QueryMsg::GetCorruptedDenoms {}), + ContractQueryMsg::Transmuter(QueryMsg::GetCorruptedScopes {}), ) .unwrap(); - let GetCorrruptedDenomsResponse { corrupted_denoms } = from_json(res).unwrap(); + let GetCorrruptedScopesResponse { corrupted_scopes } = from_json(res).unwrap(); - assert_eq!(corrupted_denoms, Vec::::new()); + assert_eq!(corrupted_scopes, Vec::::new()); // no liquidity or pool assets changes let GetTotalPoolLiquidityResponse { @@ -4119,7 +4178,7 @@ mod tests { Ok(ListAssetGroupsResponse { asset_groups: BTreeMap::from([( "group1".to_string(), - vec!["asset1".to_string(), "asset2".to_string()], + AssetGroup::new(vec!["asset1".to_string(), "asset2".to_string()]), )]), }) ); @@ -4183,9 +4242,12 @@ mod tests { asset_groups: BTreeMap::from([ ( "group1".to_string(), - vec!["asset1".to_string(), "asset2".to_string()] + AssetGroup::new(vec!["asset1".to_string(), "asset2".to_string()]) + ), + ( + "group2".to_string(), + AssetGroup::new(vec!["asset3".to_string()]) ), - ("group2".to_string(), vec!["asset3".to_string()]), ]), }) ); @@ -4269,9 +4331,12 @@ mod tests { BTreeMap::from([ ( "group1".to_string(), - vec!["asset1".to_string(), "asset2".to_string()] + AssetGroup::new(vec!["asset1".to_string(), "asset2".to_string()]) + ), + ( + "group2".to_string(), + AssetGroup::new(vec!["asset3".to_string()]) ), - ("group2".to_string(), vec!["asset3".to_string()]), ]) ); @@ -4313,7 +4378,7 @@ mod tests { list_asset_groups_res.asset_groups, BTreeMap::from([( "group1".to_string(), - vec!["asset1".to_string(), "asset2".to_string()] + AssetGroup::new(vec!["asset1".to_string(), "asset2".to_string()]) )]) ); @@ -4346,4 +4411,156 @@ mod tests { } ); } + + #[test] + fn test_mark_corrupted_scopes() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let admin = "admin"; + let moderator = "moderator"; + let user = "user"; + + // Add supply for denoms using deps.querier.update_balance + deps.querier.update_balance( + env.contract.address.clone(), + vec![Coin::new(1000000, "asset1"), Coin::new(2000000, "asset2")], + ); + + // Initialize the contract + let init_msg = InstantiateMsg { + admin: Some(admin.to_string()), + moderator: moderator.to_string(), + pool_asset_configs: vec![ + AssetConfig { + denom: "asset1".to_string(), + normalization_factor: Uint128::new(1), + }, + AssetConfig { + denom: "asset2".to_string(), + normalization_factor: Uint128::new(1), + }, + ], + alloyed_asset_subdenom: "alloyed".to_string(), + alloyed_asset_normalization_factor: Uint128::new(1), + }; + let info = mock_info(admin, &[]); + instantiate(deps.as_mut(), env.clone(), info, init_msg).unwrap(); + + // Mark corrupted scopes + let mark_corrupted_scopes_msg = ContractExecMsg::Transmuter(ExecMsg::MarkCorruptedScopes { + scopes: vec![Scope::Denom("asset1".to_string())], + }); + let moderator_info = mock_info(moderator, &[]); + let res = execute( + deps.as_mut(), + env.clone(), + moderator_info.clone(), + mark_corrupted_scopes_msg, + ) + .unwrap(); + + // Check the response + assert_eq!( + res.attributes, + vec![attr("method", "mark_corrupted_scopes")] + ); + + // Verify that the scope is marked as corrupted + // Query the contract to get the corrupted denoms + let query_msg = ContractQueryMsg::Transmuter(QueryMsg::GetCorruptedScopes {}); + let query_res: GetCorrruptedScopesResponse = + from_json(&query(deps.as_ref(), env.clone(), query_msg).unwrap()).unwrap(); + + // Check that "asset1" is in the corrupted denoms list + assert_eq!(query_res.corrupted_scopes, vec![Scope::denom("asset1")]); + + // Try to mark corrupted scopes as a non-moderator (should fail) + let user_info = mock_info(user, &[]); + let unauthorized_mark_msg = ContractExecMsg::Transmuter(ExecMsg::MarkCorruptedScopes { + scopes: vec![Scope::denom("asset2")], + }); + let err = + execute(deps.as_mut(), env.clone(), user_info, unauthorized_mark_msg).unwrap_err(); + assert_eq!(err, ContractError::Unauthorized {}); + + // Test create_asset_group + let create_asset_group_msg = ContractExecMsg::Transmuter(ExecMsg::CreateAssetGroup { + label: "group1".to_string(), + denoms: vec!["asset1".to_string(), "asset2".to_string()], + }); + let admin_info = mock_info(admin, &[]); + let res = execute( + deps.as_mut(), + env.clone(), + admin_info.clone(), + create_asset_group_msg, + ) + .unwrap(); + + // Check the response + assert_eq!( + res.attributes, + vec![ + attr("method", "create_asset_group"), + attr("label", "group1"), + ] + ); + + // Test mark_asset_group_as_corrupted + let moderator_info = mock_info(moderator, &[]); + let mark_group_corrupted_msg = ContractExecMsg::Transmuter(ExecMsg::MarkCorruptedScopes { + scopes: vec![Scope::asset_group("group1")], + }); + let res = execute( + deps.as_mut(), + env.clone(), + moderator_info.clone(), + mark_group_corrupted_msg, + ) + .unwrap(); + + // Check the response + assert_eq!( + res.attributes, + vec![attr("method", "mark_corrupted_scopes"),] + ); + + // Verify that the asset group is marked as corrupted + let query_msg = ContractQueryMsg::Transmuter(QueryMsg::GetCorruptedScopes {}); + let query_res: GetCorrruptedScopesResponse = + from_json(&query(deps.as_ref(), env.clone(), query_msg).unwrap()).unwrap(); + + // Check that both "asset1" and "asset2" are in the corrupted scopes list + assert_eq!( + query_res.corrupted_scopes, + vec![Scope::denom("asset1"), Scope::asset_group("group1")] + ); + + // Test unmark_corrupted_scopes for the asset group + let unmark_group_corrupted_msg = + ContractExecMsg::Transmuter(ExecMsg::UnmarkCorruptedScopes { + scopes: vec![Scope::asset_group("group1")], + }); + let res = execute( + deps.as_mut(), + env.clone(), + moderator_info.clone(), + unmark_group_corrupted_msg, + ) + .unwrap(); + + // Check the response + assert_eq!( + res.attributes, + vec![attr("method", "unmark_corrupted_scopes"),] + ); + + // Verify that the asset group is no longer marked as corrupted + let query_msg = ContractQueryMsg::Transmuter(QueryMsg::GetCorruptedScopes {}); + let query_res: GetCorrruptedScopesResponse = + from_json(&query(deps.as_ref(), env.clone(), query_msg).unwrap()).unwrap(); + + // Check that the asset group is no longer in the corrupted scopes list + assert_eq!(query_res.corrupted_scopes, vec![Scope::denom("asset1")]); + } } diff --git a/contracts/transmuter/src/corruptable.rs b/contracts/transmuter/src/corruptable.rs new file mode 100644 index 0000000..de16d2e --- /dev/null +++ b/contracts/transmuter/src/corruptable.rs @@ -0,0 +1,5 @@ +pub trait Corruptable { + fn is_corrupted(&self) -> bool; + fn mark_as_corrupted(&mut self) -> &mut Self; + fn unmark_as_corrupted(&mut self) -> &mut Self; +} diff --git a/contracts/transmuter/src/error.rs b/contracts/transmuter/src/error.rs index d09a055..b15ac81 100644 --- a/contracts/transmuter/src/error.rs +++ b/contracts/transmuter/src/error.rs @@ -179,8 +179,8 @@ pub enum ContractError { #[error("Normalization factor must be positive")] NormalizationFactorMustBePositive {}, - #[error("Corrupted asset: {denom} must not increase in amount or weight")] - CorruptedAssetRelativelyIncreased { denom: String }, // TODO: change this to threshold scope as well + #[error("Corrupted scope: {scope} must not increase in amount or weight")] + CorruptedScopeRelativelyIncreased { scope: Scope }, #[error("Asset group {label} not found")] AssetGroupNotFound { label: String }, diff --git a/contracts/transmuter/src/lib.rs b/contracts/transmuter/src/lib.rs index 6fc9a8c..4c4c3eb 100644 --- a/contracts/transmuter/src/lib.rs +++ b/contracts/transmuter/src/lib.rs @@ -1,6 +1,8 @@ mod alloyed_asset; mod asset; +mod asset_group; pub mod contract; +mod corruptable; mod error; mod limiter; mod math; diff --git a/contracts/transmuter/src/swap.rs b/contracts/transmuter/src/swap.rs index a28d505..f43f315 100644 --- a/contracts/transmuter/src/swap.rs +++ b/contracts/transmuter/src/swap.rs @@ -2,14 +2,15 @@ use std::collections::{BTreeMap, HashMap}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - ensure, ensure_eq, to_json_binary, Addr, BankMsg, Coin, Decimal, Deps, DepsMut, Env, Order, - Response, StdError, Storage, Uint128, + ensure, ensure_eq, to_json_binary, Addr, BankMsg, Coin, Decimal, Deps, DepsMut, Env, Response, + StdError, Storage, Uint128, }; use osmosis_std::types::osmosis::tokenfactory::v1beta1::{MsgBurn, MsgMint}; use serde::Serialize; use crate::{ alloyed_asset::{swap_from_alloyed, swap_to_alloyed}, + asset_group::AssetGroup, contract::Transmuter, scope::Scope, transmuter_pool::{AmountConstraint, TransmuterPool}, @@ -633,17 +634,19 @@ impl Transmuter<'_> { fn get_asset_group( &self, storage: &dyn Storage, - ) -> Result>, StdError> { - self.asset_group - .range(storage, None, None, Order::Ascending) - .collect::>, _>>() + ) -> Result, StdError> { + Ok(self + .asset_groups + .may_load(storage)? + .unwrap_or_default() + .into_inner()) } } fn construct_scope_value_pairs( prev_weights: BTreeMap, updated_weights: Vec<(String, Decimal)>, - asset_group: HashMap>, + asset_group: BTreeMap, ) -> Result, StdError> { let mut denom_weight_pairs: HashMap = HashMap::new(); let mut asset_group_weight_pairs: HashMap = HashMap::new(); @@ -651,8 +654,8 @@ fn construct_scope_value_pairs( // Reverse index the asset groups // TODO: handle cases where asset group contains denom that does not exist let mut asset_groups_of_denom = HashMap::new(); - for (group, denoms) in asset_group { - for denom in denoms { + for (group, asset_group) in asset_group { + for denom in asset_group.into_denoms() { asset_groups_of_denom .entry(denom) .or_insert_with(Vec::new) @@ -664,7 +667,7 @@ fn construct_scope_value_pairs( let prev_weight = prev_weights.get(denom.as_str()).unwrap_or(weight); denom_weight_pairs.insert(Scope::denom(denom), (*prev_weight, *weight)); - for group in asset_groups_of_denom.get(denom.as_str()).unwrap_or(&vec![]) { + for group in asset_groups_of_denom.get(denom).unwrap_or(&vec![]) { match asset_group_weight_pairs.get_mut(&Scope::asset_group(group)) { Some((prev, curr)) => { *prev = prev.checked_add(*prev_weight)?; @@ -1241,16 +1244,11 @@ mod tests { .pool_assets .into_iter() .map(|asset| asset.denom().to_string()) - .collect::>(); + .collect::>(); - pool.mark_corrupted_assets( - corrupted_denoms - .iter() - .map(|denom| denom.to_string()) - .collect::>() - .as_slice(), - ) - .unwrap(); + for denom in corrupted_denoms { + pool.mark_corrupted_asset(denom).unwrap(); + } transmuter.pool.save(&mut deps.storage, &pool).unwrap(); @@ -1350,7 +1348,7 @@ mod tests { .map(|asset| asset.denom().to_string()) .collect::>(); - pool.mark_corrupted_assets(&["denom1".to_owned()]).unwrap(); + pool.mark_corrupted_asset("denom1").unwrap(); transmuter.pool.save(&mut deps.storage, &pool).unwrap(); @@ -1434,7 +1432,7 @@ mod tests { .map(|asset| asset.denom().to_string()) .collect::>(); - pool.mark_corrupted_assets(&["denom1".to_owned()]).unwrap(); + pool.mark_corrupted_asset("denom1").unwrap(); transmuter.pool.save(&mut deps.storage, &pool).unwrap(); @@ -1723,13 +1721,15 @@ mod tests { .map(|(label, asset_group)| { ( label.to_string(), - asset_group - .into_iter() - .map(|asset| asset.to_string()) - .collect_vec(), + AssetGroup::new( + asset_group + .into_iter() + .map(|asset| asset.to_string()) + .collect_vec(), + ), ) }) - .collect(); + .collect::>(); let prev_weights = denom_weights .clone() @@ -1761,7 +1761,3 @@ mod tests { assert_eq!(scope_value_pairs, expected_scope_value_pairs); } } - -// TODO: -// - integration tests for asset group limiters -// - migration: prefixing exsting limiters key with "denom::" diff --git a/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs b/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs index 5dd3200..6d732ba 100644 --- a/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs +++ b/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs @@ -2,47 +2,44 @@ use std::collections::HashMap; use cosmwasm_std::{ensure, Decimal}; -use crate::{asset::Asset, ContractError}; +use crate::{asset::Asset, corruptable::Corruptable, scope::Scope, ContractError}; use super::TransmuterPool; impl TransmuterPool { - pub fn mark_corrupted_assets( - &mut self, - corrupted_denoms: &[String], - ) -> Result<(), ContractError> { - // check if removing_assets are in the pool_assets - for corrupted_denom in corrupted_denoms { - ensure!( - self.has_denom(corrupted_denom), - ContractError::InvalidPoolAssetDenom { - denom: corrupted_denom.to_string() - } - ); + pub fn mark_corrupted_asset(&mut self, corrupted_denom: &str) -> Result<(), ContractError> { + // check if denom is in the pool_assets + ensure!( + self.has_denom(corrupted_denom), + ContractError::InvalidPoolAssetDenom { + denom: corrupted_denom.to_string() + } + ); - self.pool_assets - .iter_mut() - .find(|asset| asset.denom() == corrupted_denom) - .map(|asset| asset.mark_as_corrupted()); + for asset in self.pool_assets.iter_mut() { + if asset.denom() == corrupted_denom { + asset.mark_as_corrupted(); + break; + } } Ok(()) } - pub fn unmark_corrupted_assets(&mut self, denoms: &[String]) -> Result<(), ContractError> { - // check if denoms are of corrupted assets - for uncorrupted_denom in denoms { - ensure!( - self.is_corrupted_asset(uncorrupted_denom), - ContractError::InvalidCorruptedAssetDenom { - denom: uncorrupted_denom.to_string() - } - ); + pub fn unmark_corrupted_asset(&mut self, uncorrupted_denom: &str) -> Result<(), ContractError> { + // check if denom is of corrupted asset + ensure!( + self.is_corrupted_asset(uncorrupted_denom), + ContractError::InvalidCorruptedAssetDenom { + denom: uncorrupted_denom.to_string() + } + ); - self.pool_assets - .iter_mut() - .find(|asset| asset.denom() == uncorrupted_denom) - .map(|asset| asset.unmark_as_corrupted()); + for asset in self.pool_assets.iter_mut() { + if asset.denom() == uncorrupted_denom { + asset.unmark_as_corrupted(); + break; + } } Ok(()) @@ -130,8 +127,8 @@ impl TransmuterPool { ensure!( !has_amount_increased && !has_weight_increased, - ContractError::CorruptedAssetRelativelyIncreased { - denom: post_action.denom().to_string() + ContractError::CorruptedScopeRelativelyIncreased { + scope: Scope::denom(post_action.denom()) } ); } @@ -159,9 +156,7 @@ mod tests { }; // remove asset that is not in the pool - let err = pool - .mark_corrupted_assets(&["asset5".to_string()]) - .unwrap_err(); + let err = pool.mark_corrupted_asset("asset5").unwrap_err(); assert_eq!( err, ContractError::InvalidPoolAssetDenom { @@ -169,9 +164,7 @@ mod tests { } ); - let err = pool - .mark_corrupted_assets(&["asset1".to_string(), "assetx".to_string()]) - .unwrap_err(); + let err = pool.mark_corrupted_asset("assetx").unwrap_err(); assert_eq!( err, ContractError::InvalidPoolAssetDenom { @@ -179,7 +172,7 @@ mod tests { } ); - pool.mark_corrupted_assets(&["asset1".to_string()]).unwrap(); + pool.mark_corrupted_asset("asset1").unwrap(); assert_eq!( pool.pool_assets, Asset::unchecked_equal_assets_from_coins(&[ @@ -206,8 +199,8 @@ mod tests { ] ); - pool.mark_corrupted_assets(&["asset2".to_string(), "asset3".to_string()]) - .unwrap(); + pool.mark_corrupted_asset("asset2").unwrap(); + pool.mark_corrupted_asset("asset3").unwrap(); assert_eq!( pool.pool_assets, @@ -250,7 +243,7 @@ mod tests { ]), }; - pool.mark_corrupted_assets(&["asset1".to_string()]).unwrap(); + pool.mark_corrupted_asset("asset1").unwrap(); // increase corrupted asset directly let err = pool @@ -266,8 +259,8 @@ mod tests { assert_eq!( err, - ContractError::CorruptedAssetRelativelyIncreased { - denom: "asset1".to_string() + ContractError::CorruptedScopeRelativelyIncreased { + scope: Scope::denom("asset1") } ); @@ -285,8 +278,8 @@ mod tests { assert_eq!( err, - ContractError::CorruptedAssetRelativelyIncreased { - denom: "asset1".to_string() + ContractError::CorruptedScopeRelativelyIncreased { + scope: Scope::denom("asset1") } ); @@ -307,8 +300,8 @@ mod tests { assert_eq!( err.unwrap_err(), - ContractError::CorruptedAssetRelativelyIncreased { - denom: "asset1".to_string() + ContractError::CorruptedScopeRelativelyIncreased { + scope: Scope::denom("asset1") } ); @@ -322,7 +315,7 @@ mod tests { ]), }; - pool.mark_corrupted_assets(&["asset1".to_string()]).unwrap(); + pool.mark_corrupted_asset("asset1").unwrap(); // decrease both corrupted and other asset with slightly more weight on the corrupted asset // requires slightly more weight to work due to rounding error From 91a2a8a1d6b3eefe2e321e6ec4013f2ace40124e Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Wed, 2 Oct 2024 15:16:07 +0700 Subject: [PATCH 05/26] add asset group --- contracts/transmuter/src/contract.rs | 90 ++--- contracts/transmuter/src/lib.rs | 1 - contracts/transmuter/src/swap.rs | 30 +- .../src/transmuter_pool/add_new_assets.rs | 6 + .../src/{ => transmuter_pool}/asset_group.rs | 56 ++- .../src/transmuter_pool/corrupted_assets.rs | 345 +++++++++++++++--- .../src/transmuter_pool/exit_pool.rs | 16 +- .../src/transmuter_pool/join_pool.rs | 2 +- .../transmuter/src/transmuter_pool/mod.rs | 20 +- .../src/transmuter_pool/transmute.rs | 2 +- .../transmuter/src/transmuter_pool/weight.rs | 6 +- 11 files changed, 398 insertions(+), 176 deletions(-) rename contracts/transmuter/src/{ => transmuter_pool}/asset_group.rs (83%) diff --git a/contracts/transmuter/src/contract.rs b/contracts/transmuter/src/contract.rs index aba383a..a2edb90 100644 --- a/contracts/transmuter/src/contract.rs +++ b/contracts/transmuter/src/contract.rs @@ -1,8 +1,4 @@ -use crate::{ - asset_group::{AssetGroup, AssetGroups}, - corruptable::Corruptable, - scope::Scope, -}; +use crate::{corruptable::Corruptable, scope::Scope}; use std::{collections::BTreeMap, iter}; use crate::{ @@ -14,7 +10,7 @@ use crate::{ math::{self, rescale}, role::Role, swap::{BurnTarget, Entrypoint, SwapFromAlloyedConstraint, SwapToAlloyedConstraint, SWAP_FEE}, - transmuter_pool::TransmuterPool, + transmuter_pool::{AssetGroup, TransmuterPool}, }; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ @@ -48,7 +44,6 @@ pub struct Transmuter<'a> { pub(crate) alloyed_asset: AlloyedAsset<'a>, pub(crate) role: Role<'a>, pub(crate) limiters: Limiters<'a>, - pub(crate) asset_groups: Item<'a, AssetGroups>, } pub mod key { @@ -76,7 +71,6 @@ impl Transmuter<'_> { ), role: Role::new(key::ADMIN, key::MODERATOR), limiters: Limiters::new(key::LIMITERS), - asset_groups: Item::new(key::ASSET_GROUP), } } @@ -141,9 +135,6 @@ impl Transmuter<'_> { self.alloyed_asset .set_normalization_factor(deps.storage, alloyed_asset_normalization_factor)?; - // initialize asset groups - self.asset_groups.save(deps.storage, &AssetGroups::new())?; - Ok(Response::new() .add_attribute("method", "instantiate") .add_attribute("contract_name", CONTRACT_NAME) @@ -261,25 +252,12 @@ impl Transmuter<'_> { // only admin can create asset group ensure_admin_authority!(info.sender, self.role.admin, deps.as_ref()); - // ensure that all denoms are valid pool assets - let pool = self.pool.load(deps.storage)?; - for denom in &denoms { - ensure!( - pool.has_denom(denom), - ContractError::InvalidPoolAssetDenom { - denom: denom.clone() - } - ); - } - // create asset group - let mut asset_groups = self - .asset_groups - .may_load(deps.storage)? - .unwrap_or_default(); - - asset_groups.create_asset_group(label.clone(), denoms)?; - self.asset_groups.save(deps.storage, &asset_groups)?; + self.pool + .update(deps.storage, |mut pool| -> Result<_, ContractError> { + pool.create_asset_group(label.clone(), denoms)?; + Ok(pool) + })?; Ok(Response::new() .add_attribute("method", "create_asset_group") @@ -297,13 +275,11 @@ impl Transmuter<'_> { // only admin can remove asset group ensure_admin_authority!(info.sender, self.role.admin, deps.as_ref()); - let mut asset_groups = self - .asset_groups - .may_load(deps.storage)? - .unwrap_or_default(); - - asset_groups.remove_asset_group(&label)?; - self.asset_groups.save(deps.storage, &asset_groups)?; + self.pool + .update(deps.storage, |mut pool| -> Result<_, ContractError> { + pool.remove_asset_group(&label)?; + Ok(pool) + })?; // remove all limiters for asset group let limiters = self @@ -342,10 +318,6 @@ impl Transmuter<'_> { ensure_moderator_authority!(info.sender, self.role.moderator, deps.as_ref()); let mut pool = self.pool.load(deps.storage)?; - let mut asset_groups = self - .asset_groups - .may_load(deps.storage)? - .unwrap_or_default(); for scope in &scopes { match scope { @@ -353,13 +325,12 @@ impl Transmuter<'_> { pool.mark_corrupted_asset(denom)?; } Scope::AssetGroup(label) => { - asset_groups.mark_corrupted_asset_group(label)?; + pool.mark_corrupted_asset_group(label)?; } } } self.pool.save(deps.storage, &pool)?; - self.asset_groups.save(deps.storage, &asset_groups)?; Ok(Response::new().add_attribute("method", "mark_corrupted_scopes")) } @@ -377,10 +348,6 @@ impl Transmuter<'_> { ensure_moderator_authority!(info.sender, self.role.moderator, deps.as_ref()); let mut pool = self.pool.load(deps.storage)?; - let mut asset_groups = self - .asset_groups - .may_load(deps.storage)? - .unwrap_or_default(); for scope in &scopes { match scope { @@ -388,13 +355,12 @@ impl Transmuter<'_> { pool.unmark_corrupted_asset(denom)?; } Scope::AssetGroup(label) => { - asset_groups.unmark_corrupted_asset_group(label)?; + pool.unmark_corrupted_asset_group(label)?; } } } self.pool.save(deps.storage, &pool)?; - self.asset_groups.save(deps.storage, &asset_groups)?; Ok(Response::new().add_attribute("method", "unmark_corrupted_scopes")) } @@ -412,23 +378,20 @@ impl Transmuter<'_> { // only admin can register limiter ensure_admin_authority!(info.sender, self.role.admin, deps.as_ref()); - let asset_groups = self - .asset_groups - .may_load(deps.storage)? - .unwrap_or_default(); + let pool = self.pool.load(deps.storage)?; match scope.clone() { Scope::Denom(denom) => { // ensure pool has the specified denom ensure!( - self.pool.load(deps.storage)?.has_denom(&denom), + pool.has_denom(&denom), ContractError::InvalidPoolAssetDenom { denom } ); } Scope::AssetGroup(label) => { // check if asset group exists ensure!( - asset_groups.has(&label), + pool.has_asset_group(&label), ContractError::AssetGroupNotFound { label } ); } @@ -701,13 +664,10 @@ impl Transmuter<'_> { &self, QueryCtx { deps, env: _ }: QueryCtx, ) -> Result { - let asset_groups = self - .asset_groups - .may_load(deps.storage)? - .unwrap_or_default(); + let pool = self.pool.load(deps.storage)?; Ok(ListAssetGroupsResponse { - asset_groups: asset_groups.into_inner(), + asset_groups: pool.asset_groups, }) } @@ -860,21 +820,17 @@ impl Transmuter<'_> { QueryCtx { deps, env: _ }: QueryCtx, ) -> Result { let pool = self.pool.load(deps.storage)?; - let asset_groups = self - .asset_groups - .may_load(deps.storage)? - .unwrap_or_default(); let corrupted_assets = pool .corrupted_assets() .into_iter() .map(|a| Scope::denom(a.denom())); - let corrupted_asset_groups = asset_groups - .into_inner() - .into_iter() + let corrupted_asset_groups = pool + .asset_groups + .iter() .filter(|(_, asset_group)| asset_group.is_corrupted()) - .map(|(label, _)| Scope::AssetGroup(label)); + .map(|(label, _)| Scope::asset_group(label)); Ok(GetCorrruptedScopesResponse { corrupted_scopes: corrupted_assets.chain(corrupted_asset_groups).collect(), diff --git a/contracts/transmuter/src/lib.rs b/contracts/transmuter/src/lib.rs index 4c4c3eb..06760db 100644 --- a/contracts/transmuter/src/lib.rs +++ b/contracts/transmuter/src/lib.rs @@ -1,6 +1,5 @@ mod alloyed_asset; mod asset; -mod asset_group; pub mod contract; mod corruptable; mod error; diff --git a/contracts/transmuter/src/swap.rs b/contracts/transmuter/src/swap.rs index f43f315..84dafcf 100644 --- a/contracts/transmuter/src/swap.rs +++ b/contracts/transmuter/src/swap.rs @@ -10,10 +10,9 @@ use serde::Serialize; use crate::{ alloyed_asset::{swap_from_alloyed, swap_to_alloyed}, - asset_group::AssetGroup, contract::Transmuter, scope::Scope, - transmuter_pool::{AmountConstraint, TransmuterPool}, + transmuter_pool::{AmountConstraint, AssetGroup, TransmuterPool}, ContractError, }; @@ -135,7 +134,7 @@ impl Transmuter<'_> { let scope_value_pairs = construct_scope_value_pairs( prev_weights, updated_weights, - self.get_asset_group(deps.storage)?, + pool.asset_groups.clone(), )?; self.limiters.check_limits_and_update( @@ -317,7 +316,7 @@ impl Transmuter<'_> { let scope_value_pairs = construct_scope_value_pairs( prev_weights, updated_weights, - self.get_asset_group(deps.storage)?, + pool.asset_groups.clone(), )?; self.limiters.check_limits_and_update( @@ -382,7 +381,7 @@ impl Transmuter<'_> { let scope_value_pairs = construct_scope_value_pairs( prev_weights, updated_weights, - self.get_asset_group(deps.storage)?, + pool.asset_groups.clone(), )?; self.limiters.check_limits_and_update( @@ -443,7 +442,7 @@ impl Transmuter<'_> { let scope_value_pairs = construct_scope_value_pairs( prev_weights, updated_weights, - self.get_asset_group(deps.storage)?, + pool.asset_groups.clone(), )?; self.limiters.check_limits_and_update( deps.storage, @@ -629,18 +628,6 @@ impl Transmuter<'_> { Ok(()) } - - /// get asset group mapping from storage - fn get_asset_group( - &self, - storage: &dyn Storage, - ) -> Result, StdError> { - Ok(self - .asset_groups - .may_load(storage)? - .unwrap_or_default() - .into_inner()) - } } fn construct_scope_value_pairs( @@ -901,6 +888,7 @@ mod tests { Asset::new(Uint128::from(1000u128), "denom1", 1u128).unwrap(), Asset::new(Uint128::from(1000u128), "denom2", 10u128).unwrap(), ], + asset_groups: BTreeMap::new(), }, ) .unwrap(); @@ -1038,6 +1026,7 @@ mod tests { Asset::new(Uint128::from(1000000000000u128), "denom1", 1u128).unwrap(), Asset::new(Uint128::from(1000000000000u128), "denom2", 10u128).unwrap(), ], + asset_groups: BTreeMap::new(), }, ) .unwrap(); @@ -1237,6 +1226,7 @@ mod tests { Asset::new(Uint128::from(1000000000000u128), "denom2", 10u128).unwrap(), // 1000000000000 * 10 Asset::new(Uint128::from(1000000000000u128), "denom3", 1u128).unwrap(), // 1000000000000 * 100 ], + asset_groups: BTreeMap::new(), }; let all_denoms = pool @@ -1339,6 +1329,7 @@ mod tests { Asset::new(Uint128::from(1000000000000u128), "denom2", 10u128).unwrap(), // 1000000000000 * 10 Asset::new(Uint128::from(1000000000000u128), "denom3", 1u128).unwrap(), // 1000000000000 * 100 ], + asset_groups: BTreeMap::new(), }; let all_denoms = pool @@ -1423,6 +1414,7 @@ mod tests { Asset::new(Uint128::from(1000000000000u128), "denom2", 10u128).unwrap(), // 1000000000000 * 10 Asset::new(Uint128::from(1000000000000u128), "denom3", 1u128).unwrap(), // 1000000000000 * 100 ], + asset_groups: BTreeMap::new(), }; let all_denoms = pool @@ -1568,6 +1560,7 @@ mod tests { Asset::new(Uint128::from(1000000000000u128), "denom1", 1u128).unwrap(), Asset::new(Uint128::from(1000000000000u128), "denom2", 10u128).unwrap(), ], + asset_groups: BTreeMap::new(), }, ) .unwrap(); @@ -1665,6 +1658,7 @@ mod tests { Asset::new(Uint128::from(1000000000000u128), "denom1", 1u128).unwrap(), Asset::new(Uint128::from(1000000000000u128), "denom2", 10u128).unwrap(), ], + asset_groups: BTreeMap::new(), }, ) .unwrap(); diff --git a/contracts/transmuter/src/transmuter_pool/add_new_assets.rs b/contracts/transmuter/src/transmuter_pool/add_new_assets.rs index b245d58..e3cf377 100644 --- a/contracts/transmuter/src/transmuter_pool/add_new_assets.rs +++ b/contracts/transmuter/src/transmuter_pool/add_new_assets.rs @@ -13,6 +13,8 @@ impl TransmuterPool { #[cfg(test)] mod tests { + use std::collections::BTreeMap; + use cosmwasm_std::{Coin, Uint128, Uint64}; use crate::transmuter_pool::{MAX_POOL_ASSET_DENOMS, MIN_POOL_ASSET_DENOMS}; @@ -26,6 +28,7 @@ mod tests { Coin::new(100000000, "asset1"), Coin::new(99999999, "asset2"), ]), + asset_groups: BTreeMap::new(), }; let new_assets = Asset::unchecked_equal_assets_from_coins(&[ Coin::new(0, "asset3"), @@ -50,6 +53,7 @@ mod tests { Coin::new(100000000, "asset1"), Coin::new(99999999, "asset2"), ]), + asset_groups: BTreeMap::new(), }; let new_assets = Asset::unchecked_equal_assets_from_coins(&[ Coin::new(0, "asset3"), @@ -72,6 +76,7 @@ mod tests { Coin::new(100000000, "asset1"), Coin::new(99999999, "asset2"), ]), + asset_groups: BTreeMap::new(), }; let err = pool .add_new_assets(Asset::unchecked_equal_assets_from_coins(&[ @@ -94,6 +99,7 @@ mod tests { Coin::new(100000000, "asset1"), Coin::new(99999999, "asset2"), ]), + asset_groups: BTreeMap::new(), }; let new_assets = Asset::unchecked_equal_assets_from_coins(&[ Coin::new(0, "asset3"), diff --git a/contracts/transmuter/src/asset_group.rs b/contracts/transmuter/src/transmuter_pool/asset_group.rs similarity index 83% rename from contracts/transmuter/src/asset_group.rs rename to contracts/transmuter/src/transmuter_pool/asset_group.rs index 198480a..09190b3 100644 --- a/contracts/transmuter/src/asset_group.rs +++ b/contracts/transmuter/src/transmuter_pool/asset_group.rs @@ -1,10 +1,10 @@ -use std::collections::BTreeMap; - use cosmwasm_schema::cw_serde; use cosmwasm_std::ensure; use crate::{corruptable::Corruptable, ContractError}; +use super::TransmuterPool; + #[cw_serde] pub struct AssetGroup { denoms: Vec, @@ -54,28 +54,13 @@ impl Corruptable for AssetGroup { } } -#[cw_serde] -pub struct AssetGroups(BTreeMap); - -impl Default for AssetGroups { - fn default() -> Self { - Self::new() - } -} - -impl AssetGroups { - pub fn new() -> Self { - Self(BTreeMap::new()) - } - - pub fn has(&self, label: &str) -> bool { - self.0.contains_key(label) +impl TransmuterPool { + pub fn has_asset_group(&self, label: &str) -> bool { + self.asset_groups.contains_key(label) } pub fn mark_corrupted_asset_group(&mut self, label: &str) -> Result<&mut Self, ContractError> { - let Self(asset_groups) = self; - - asset_groups + self.asset_groups .get_mut(label) .ok_or_else(|| ContractError::AssetGroupNotFound { label: label.to_string(), @@ -89,9 +74,7 @@ impl AssetGroups { &mut self, label: &str, ) -> Result<&mut Self, ContractError> { - let Self(asset_groups) = self; - - asset_groups + self.asset_groups .get_mut(label) .ok_or_else(|| ContractError::AssetGroupNotFound { label: label.to_string(), @@ -106,25 +89,32 @@ impl AssetGroups { label: String, denoms: Vec, ) -> Result<&mut Self, ContractError> { - let Self(asset_groups) = self; - + // ensure that asset group does not already exist ensure!( - !asset_groups.contains_key(&label), + !self.asset_groups.contains_key(&label), ContractError::AssetGroupAlreadyExists { label: label.clone() } ); - asset_groups.insert(label, AssetGroup::new(denoms)); + // ensure that all denoms are valid pool assets + for denom in &denoms { + ensure!( + self.has_denom(denom), + ContractError::InvalidPoolAssetDenom { + denom: denom.clone() + } + ); + } + + self.asset_groups.insert(label, AssetGroup::new(denoms)); Ok(self) } pub fn remove_asset_group(&mut self, label: &str) -> Result<&mut Self, ContractError> { - let Self(asset_groups) = self; - ensure!( - asset_groups.remove(label).is_some(), + self.asset_groups.remove(label).is_some(), ContractError::AssetGroupNotFound { label: label.to_string() } @@ -132,10 +122,6 @@ impl AssetGroups { Ok(self) } - - pub fn into_inner(self) -> BTreeMap { - self.0 - } } #[cfg(test)] diff --git a/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs b/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs index 6d732ba..9b8f095 100644 --- a/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs +++ b/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs @@ -1,11 +1,10 @@ -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; -use cosmwasm_std::{ensure, Decimal}; +use cosmwasm_std::{ensure, Decimal, Uint128}; +use super::{asset_group::AssetGroup, TransmuterPool}; use crate::{asset::Asset, corruptable::Corruptable, scope::Scope, ContractError}; -use super::TransmuterPool; - impl TransmuterPool { pub fn mark_corrupted_asset(&mut self, corrupted_denom: &str) -> Result<(), ContractError> { // check if denom is in the pool_assets @@ -78,9 +77,9 @@ impl TransmuterPool { Ok(()) } - /// Enforce corrupted assets protocol on specific action. This will ensure that amount or weight - /// of corrupted assets will never be increased. - pub fn with_corrupted_asset_protocol(&mut self, action: A) -> Result + /// Enforce corrupted scopes protocol on specific action. This will ensure that amount or weight + /// of corrupted scopes will never be increased. + pub fn with_corrupted_scopes_protocol(&mut self, action: A) -> Result where A: FnOnce(&mut Self) -> Result, { @@ -135,6 +134,144 @@ impl TransmuterPool { Ok(res) } + + /// Enforce corrupted scopes protocol on specific action. This will ensure that amount or weight + /// of corrupted scopes will never be increased. + pub fn _with_corrupted_scopes_protocol( + &mut self, + asset_groups: BTreeMap, + action: A, + ) -> Result + where + A: FnOnce(&mut Self) -> Result, + { + let corrupted_assets_pre_action = self.get_corrupted_assets(); + let corrupted_asset_groups = self.get_corrupted_asset_groups(asset_groups); + + // early return result without any checks if no corrupted scope + if corrupted_assets_pre_action.is_empty() && corrupted_asset_groups.is_empty() { + return action(self); + } + + let weight_pre_action = self.get_weights()?; + let corrupted_asset_groups_state_pre_action = + self.get_corrupted_asset_groups_state(&corrupted_asset_groups, &weight_pre_action)?; + + let res = action(self)?; + + let corrupted_assets_post_action = self.get_corrupted_assets(); + let weight_post_action = self.get_weights()?; + let corrupted_asset_groups_state_post_action = + self.get_corrupted_asset_groups_state(&corrupted_asset_groups, &weight_post_action)?; + + self.check_corrupted_assets( + &corrupted_assets_pre_action, + &corrupted_assets_post_action, + &weight_pre_action, + &weight_post_action, + )?; + + self.check_corrupted_asset_groups( + &corrupted_asset_groups_state_pre_action, + &corrupted_asset_groups_state_post_action, + )?; + + Ok(res) + } + + fn get_corrupted_assets(&self) -> HashMap { + self.pool_assets + .iter() + .filter(|asset| asset.is_corrupted()) + .map(|asset| (asset.denom().to_string(), asset.clone())) + .collect() + } + + fn get_corrupted_asset_groups( + &self, + asset_groups: BTreeMap, + ) -> BTreeMap { + asset_groups + .into_iter() + .filter(|(_, asset_group)| asset_group.is_corrupted()) + .collect() + } + + fn get_weights(&self) -> Result, ContractError> { + Ok(self.weights()?.unwrap_or_default().into_iter().collect()) + } + + /// Get the state of corrupted asset groups. + /// returns map for label -> (amount, weight) for each asset group + fn get_corrupted_asset_groups_state( + &self, + corrupted_asset_groups: &BTreeMap, + weights: &HashMap, + ) -> Result, ContractError> { + corrupted_asset_groups + .iter() + .map(|(label, asset_group)| -> Result<_, ContractError> { + let (amount, weight) = asset_group.denoms().iter().try_fold( + (Uint128::zero(), Decimal::zero()), + |(acc_amount, acc_weight), denom| -> Result<_, ContractError> { + let asset = self.get_pool_asset_by_denom(denom)?; + let amount = acc_amount.checked_add(asset.amount())?; + let weight = acc_weight + .checked_add(*weights.get(denom).unwrap_or(&Decimal::zero()))?; + Ok((amount, weight)) + }, + )?; + Ok((label.clone(), (amount, weight))) + }) + .collect() + } + + fn check_corrupted_assets( + &self, + pre_action: &HashMap, + post_action: &HashMap, + weight_pre_action: &HashMap, + weight_post_action: &HashMap, + ) -> Result<(), ContractError> { + let zero_dec = Decimal::zero(); + for (denom, post_asset) in post_action { + let pre_asset = pre_action.get(denom).ok_or(ContractError::Never)?; + let weight_pre = weight_pre_action.get(denom).unwrap_or(&zero_dec); + let weight_post = weight_post_action.get(denom).unwrap_or(&zero_dec); + + let has_amount_increased = pre_asset.amount() < post_asset.amount(); + let has_weight_increased = weight_pre < weight_post; + + ensure!( + !has_amount_increased && !has_weight_increased, + ContractError::CorruptedScopeRelativelyIncreased { + scope: Scope::denom(post_asset.denom()) + } + ); + } + Ok(()) + } + + fn check_corrupted_asset_groups( + &self, + pre_action: &BTreeMap, + post_action: &BTreeMap, + ) -> Result<(), ContractError> { + for (label, (pre_amount, pre_weight)) in pre_action { + let (post_amount, post_weight) = post_action.get(label).ok_or(ContractError::Never)?; + + let has_amount_increased = pre_amount < post_amount; + let has_weight_increased = pre_weight < post_weight; + + ensure!( + !has_amount_increased && !has_weight_increased, + ContractError::CorruptedScopeRelativelyIncreased { + scope: Scope::asset_group(label) + } + ); + } + Ok(()) + } } #[cfg(test)] @@ -153,6 +290,7 @@ mod tests { Coin::new(1, "asset3"), Coin::new(0, "asset4"), ]), + asset_groups: BTreeMap::new(), }; // remove asset that is not in the pool @@ -241,18 +379,19 @@ mod tests { Coin::new(1, "asset3"), Coin::new(0, "asset4"), ]), + asset_groups: BTreeMap::new(), }; pool.mark_corrupted_asset("asset1").unwrap(); // increase corrupted asset directly let err = pool - .with_corrupted_asset_protocol(|pool| { - pool.pool_assets - .iter_mut() - .find(|asset| asset.denom() == "asset1") - .map(|asset| asset.increase_amount(Uint128::new(1)).unwrap()) - .unwrap(); + ._with_corrupted_scopes_protocol(BTreeMap::new(), |pool| { + for asset in pool.pool_assets.iter_mut() { + if asset.denom() == "asset1" { + asset.increase_amount(Uint128::new(1)).unwrap(); + } + } Ok(()) }) .unwrap_err(); @@ -266,12 +405,12 @@ mod tests { // decrease other asset -> increase corrupted asset weight let err = pool - .with_corrupted_asset_protocol(|pool| { - pool.pool_assets - .iter_mut() - .find(|asset| asset.denom() == "asset2") - .map(|asset| asset.decrease_amount(Uint128::new(1)).unwrap()) - .unwrap(); + ._with_corrupted_scopes_protocol(BTreeMap::new(), |pool| { + for asset in pool.pool_assets.iter_mut() { + if asset.denom() == "asset2" { + asset.decrease_amount(Uint128::new(1)).unwrap(); + } + } Ok(()) }) .unwrap_err(); @@ -284,22 +423,24 @@ mod tests { ); // decrease both corrupted and other asset with different weight - let err = pool.with_corrupted_asset_protocol(|pool| { - pool.pool_assets - .iter_mut() - .find(|asset| asset.denom() == "asset1") - .map(|asset| asset.decrease_amount(Uint128::new(1)).unwrap()) - .unwrap(); - pool.pool_assets - .iter_mut() - .find(|asset| asset.denom() == "asset2") - .map(|asset| asset.decrease_amount(Uint128::new(2)).unwrap()) - .unwrap(); - Ok(()) - }); + let err = pool + ._with_corrupted_scopes_protocol(BTreeMap::new(), |pool| { + for asset in pool.pool_assets.iter_mut() { + if asset.denom() == "asset1" { + asset.decrease_amount(Uint128::new(1)).unwrap(); + } + + if asset.denom() == "asset2" { + asset.decrease_amount(Uint128::new(2)).unwrap(); + } + } + + Ok(()) + }) + .unwrap_err(); assert_eq!( - err.unwrap_err(), + err, ContractError::CorruptedScopeRelativelyIncreased { scope: Scope::denom("asset1") } @@ -313,23 +454,23 @@ mod tests { Coin::new(1, "asset3"), Coin::new(0, "asset4"), ]), + asset_groups: BTreeMap::new(), }; pool.mark_corrupted_asset("asset1").unwrap(); // decrease both corrupted and other asset with slightly more weight on the corrupted asset // requires slightly more weight to work due to rounding error - pool.with_corrupted_asset_protocol(|pool| { - pool.pool_assets - .iter_mut() - .find(|asset| asset.denom() == "asset1") - .map(|asset| asset.decrease_amount(Uint128::new(2)).unwrap()) - .unwrap(); - pool.pool_assets - .iter_mut() - .find(|asset| asset.denom() == "asset2") - .map(|asset| asset.decrease_amount(Uint128::new(1)).unwrap()) - .unwrap(); + pool._with_corrupted_scopes_protocol(BTreeMap::new(), |pool| { + for asset in pool.pool_assets.iter_mut() { + if asset.denom() == "asset1" { + asset.decrease_amount(Uint128::new(2)).unwrap(); + } + + if asset.denom() == "asset2" { + asset.decrease_amount(Uint128::new(1)).unwrap(); + } + } Ok(()) }) .unwrap(); @@ -352,5 +493,121 @@ mod tests { }) .collect::>() ); + + let mut asset_groups = BTreeMap::from_iter(vec![( + "group1".to_string(), + AssetGroup::new(vec!["asset2".to_string(), "asset3".to_string()]), + )]); + + // increase asset in non-corrupted asset group + pool._with_corrupted_scopes_protocol(asset_groups.clone(), |pool| { + for asset in pool.pool_assets.iter_mut() { + if asset.denom() == "asset2" { + asset.increase_amount(Uint128::new(1)).unwrap(); + } + } + + Ok(()) + }) + .unwrap(); + + asset_groups.get_mut("group1").unwrap().mark_as_corrupted(); + + // increase asset in corrupted asset group + let err = pool + ._with_corrupted_scopes_protocol(asset_groups.clone(), |pool| { + for asset in pool.pool_assets.iter_mut() { + if asset.denom() == "asset2" { + asset.increase_amount(Uint128::new(1)).unwrap(); + } + } + Ok(()) + }) + .unwrap_err(); + + assert_eq!( + err, + ContractError::CorruptedScopeRelativelyIncreased { + scope: Scope::asset_group("group1") + } + ); + + // increase all assets except asset1 + let err = pool + ._with_corrupted_scopes_protocol(asset_groups.clone(), |pool| { + for asset in pool.pool_assets.iter_mut() { + if asset.denom() != "asset1" { + asset.increase_amount(Uint128::new(1)).unwrap(); + } + } + + Ok(()) + }) + .unwrap_err(); + + assert_eq!( + err, + ContractError::CorruptedScopeRelativelyIncreased { + scope: Scope::asset_group("group1") + } + ); + + pool.unmark_corrupted_asset("asset1").unwrap(); + + // decrease asset 2 + pool._with_corrupted_scopes_protocol(asset_groups.clone(), |pool| { + for asset in pool.pool_assets.iter_mut() { + if asset.denom() == "asset2" { + asset.decrease_amount(Uint128::new(1)).unwrap(); + } + } + Ok(()) + }) + .unwrap(); + + // decrease asset 3 + pool._with_corrupted_scopes_protocol(asset_groups.clone(), |pool| { + for asset in pool.pool_assets.iter_mut() { + if asset.denom() == "asset3" { + asset.decrease_amount(Uint128::new(1)).unwrap(); + } + } + Ok(()) + }) + .unwrap(); + + // decrease asset 2 and 3 + pool._with_corrupted_scopes_protocol(asset_groups.clone(), |pool| { + for asset in pool.pool_assets.iter_mut() { + if asset.denom() == "asset2" { + asset.decrease_amount(Uint128::new(1)).unwrap(); + } + + if asset.denom() == "asset3" { + asset.decrease_amount(Uint128::new(1)).unwrap(); + } + } + Ok(()) + }) + .unwrap(); + + // decrease asset 4 should fail + let err = pool + ._with_corrupted_scopes_protocol(asset_groups.clone(), |pool| { + for asset in pool.pool_assets.iter_mut() { + if asset.denom() == "asset4" { + asset.decrease_amount(Uint128::new(1)).unwrap(); + } + } + Ok(()) + }) + .unwrap_err(); + + assert_eq!( + err, + ContractError::CorruptedScopeRelativelyIncreased { + scope: Scope::asset_group("group1") + } + ); } } diff --git a/contracts/transmuter/src/transmuter_pool/exit_pool.rs b/contracts/transmuter/src/transmuter_pool/exit_pool.rs index 56114d5..a0975ec 100644 --- a/contracts/transmuter/src/transmuter_pool/exit_pool.rs +++ b/contracts/transmuter/src/transmuter_pool/exit_pool.rs @@ -6,7 +6,7 @@ use super::TransmuterPool; impl TransmuterPool { pub fn exit_pool(&mut self, tokens_out: &[Coin]) -> Result<(), ContractError> { - self.with_corrupted_asset_protocol(|pool| pool.unchecked_exit_pool(tokens_out)) + self.with_corrupted_scopes_protocol(|pool| pool.unchecked_exit_pool(tokens_out)) } pub fn unchecked_exit_pool(&mut self, tokens_out: &[Coin]) -> Result<(), ContractError> { @@ -44,6 +44,8 @@ impl TransmuterPool { #[cfg(test)] mod tests { + use std::collections::BTreeMap; + use crate::asset::Asset; use super::*; @@ -57,6 +59,7 @@ mod tests { Coin::new(100_000, ETH_USDC), Coin::new(100_000, COSMOS_USDC), ]), + asset_groups: BTreeMap::new(), }; // exit pool with first token @@ -67,7 +70,8 @@ mod tests { pool_assets: Asset::unchecked_equal_assets_from_coins(&[ Coin::new(90_000, ETH_USDC), Coin::new(100_000, COSMOS_USDC), - ]) + ]), + asset_groups: BTreeMap::new(), } ); @@ -79,7 +83,8 @@ mod tests { pool_assets: Asset::unchecked_equal_assets_from_coins(&[ Coin::new(90_000, ETH_USDC), Coin::new(90_000, COSMOS_USDC), - ]) + ]), + asset_groups: BTreeMap::new(), } ); @@ -92,7 +97,8 @@ mod tests { pool_assets: Asset::unchecked_equal_assets_from_coins(&[ Coin::new(0, ETH_USDC), Coin::new(0, COSMOS_USDC), - ]) + ]), + asset_groups: BTreeMap::new(), } ); } @@ -104,6 +110,7 @@ mod tests { Coin::new(100_000, ETH_USDC), Coin::new(100_000, COSMOS_USDC), ]), + asset_groups: BTreeMap::new(), }; // exit pool with invalid token @@ -134,6 +141,7 @@ mod tests { Coin::new(100_000, ETH_USDC), Coin::new(100_000, COSMOS_USDC), ]), + asset_groups: BTreeMap::new(), }; let err = pool.exit_pool(&[Coin::new(100_001, ETH_USDC)]).unwrap_err(); diff --git a/contracts/transmuter/src/transmuter_pool/join_pool.rs b/contracts/transmuter/src/transmuter_pool/join_pool.rs index 4c993b8..68a702c 100644 --- a/contracts/transmuter/src/transmuter_pool/join_pool.rs +++ b/contracts/transmuter/src/transmuter_pool/join_pool.rs @@ -6,7 +6,7 @@ use super::TransmuterPool; impl TransmuterPool { pub fn join_pool(&mut self, tokens_in: &[Coin]) -> Result<(), ContractError> { - self.with_corrupted_asset_protocol(|pool| pool.unchecked_join_pool(tokens_in)) + self.with_corrupted_scopes_protocol(|pool| pool.unchecked_join_pool(tokens_in)) } fn unchecked_join_pool(&mut self, tokens_in: &[Coin]) -> Result<(), ContractError> { diff --git a/contracts/transmuter/src/transmuter_pool/mod.rs b/contracts/transmuter/src/transmuter_pool/mod.rs index 6205219..b503db8 100644 --- a/contracts/transmuter/src/transmuter_pool/mod.rs +++ b/contracts/transmuter/src/transmuter_pool/mod.rs @@ -1,4 +1,5 @@ mod add_new_assets; +mod asset_group; mod corrupted_assets; mod exit_pool; mod has_denom; @@ -6,13 +7,14 @@ mod join_pool; mod transmute; mod weight; -use std::collections::HashSet; +use std::collections::{BTreeMap, HashSet}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ensure, Coin, Uint128, Uint64}; use crate::{asset::Asset, ContractError}; +pub use asset_group::AssetGroup; pub use transmute::AmountConstraint; /// Minimum number of pool assets. @@ -26,11 +28,15 @@ const MAX_POOL_ASSET_DENOMS: Uint64 = Uint64::new(20u64); #[cw_serde] pub struct TransmuterPool { pub pool_assets: Vec, + pub asset_groups: BTreeMap, } impl TransmuterPool { pub fn new(pool_assets: Vec) -> Result { - let pool = Self { pool_assets }; + let pool = Self { + pool_assets, + asset_groups: BTreeMap::new(), + }; pool.ensure_no_duplicated_denom()?; pool.ensure_pool_asset_count_within_range()?; @@ -111,7 +117,10 @@ impl TransmuterPool { }) .collect::, ContractError>>()?; - Ok(Self { pool_assets }) + Ok(Self { + pool_assets, + ..self + }) } } @@ -138,6 +147,7 @@ mod tests { TransmuterPool::new(Asset::unchecked_equal_assets(&["a"])).unwrap(), TransmuterPool { pool_assets: Asset::unchecked_equal_assets(&["a"]), + asset_groups: BTreeMap::new(), } ); @@ -146,6 +156,7 @@ mod tests { TransmuterPool::new(Asset::unchecked_equal_assets(&["a", "b"])).unwrap(), TransmuterPool { pool_assets: Asset::unchecked_equal_assets(&["a", "b"]), + asset_groups: BTreeMap::new(), } ); @@ -156,7 +167,8 @@ mod tests { assert_eq!( TransmuterPool::new(assets.clone()).unwrap(), TransmuterPool { - pool_assets: assets + pool_assets: assets, + asset_groups: BTreeMap::new(), } ); diff --git a/contracts/transmuter/src/transmuter_pool/transmute.rs b/contracts/transmuter/src/transmuter_pool/transmute.rs index 25f8f5e..37ec142 100644 --- a/contracts/transmuter/src/transmuter_pool/transmute.rs +++ b/contracts/transmuter/src/transmuter_pool/transmute.rs @@ -30,7 +30,7 @@ impl TransmuterPool { token_in_denom: &str, token_out_denom: &str, ) -> Result<(Coin, Coin), ContractError> { - self.with_corrupted_asset_protocol(|pool| { + self.with_corrupted_scopes_protocol(|pool| { pool.unchecked_transmute(amount_constraint, token_in_denom, token_out_denom) }) } diff --git a/contracts/transmuter/src/transmuter_pool/weight.rs b/contracts/transmuter/src/transmuter_pool/weight.rs index 5125ca8..08d53db 100644 --- a/contracts/transmuter/src/transmuter_pool/weight.rs +++ b/contracts/transmuter/src/transmuter_pool/weight.rs @@ -171,7 +171,10 @@ mod tests { .into_iter() .map(|asset| asset.unwrap()) .collect(); - let pool = TransmuterPool { pool_assets }; + let pool = TransmuterPool { + pool_assets, + asset_groups: BTreeMap::new(), + }; let ratios = pool.weights().unwrap(); assert_eq!(ratios, Some(expected)); @@ -184,6 +187,7 @@ mod tests { Coin::new(0, "axlusdc"), Coin::new(0, "whusdc"), ]), + asset_groups: BTreeMap::new(), }; let ratios = pool.weights().unwrap(); From 48520dc8c91d131c7ccee4c991d0047a6294ffe0 Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Wed, 2 Oct 2024 15:59:28 +0700 Subject: [PATCH 06/26] comment out current migration test --- .../src/test/cases/units/migrate.rs | 568 +++++++++--------- 1 file changed, 284 insertions(+), 284 deletions(-) diff --git a/contracts/transmuter/src/test/cases/units/migrate.rs b/contracts/transmuter/src/test/cases/units/migrate.rs index ae35b4f..81d5ec3 100644 --- a/contracts/transmuter/src/test/cases/units/migrate.rs +++ b/contracts/transmuter/src/test/cases/units/migrate.rs @@ -1,284 +1,284 @@ -use std::{iter, path::PathBuf}; - -use crate::{ - asset::AssetConfig, - contract::{ - sv::{InstantiateMsg, QueryMsg}, - GetModeratorResponse, ListAssetConfigsResponse, - }, - migrations::v3_2_0::MigrateMsg, - test::{modules::cosmwasm_pool::CosmwasmPool, test_env::TransmuterContract}, -}; -use cosmwasm_schema::cw_serde; -use cosmwasm_std::{from_json, to_json_binary, Coin, Uint128}; -use osmosis_std::types::{ - cosmwasm::wasm::v1::{QueryRawContractStateRequest, QueryRawContractStateResponse}, - osmosis::cosmwasmpool::v1beta1::{ - ContractInfoByPoolIdRequest, ContractInfoByPoolIdResponse, MigratePoolContractsProposal, - MsgCreateCosmWasmPool, UploadCosmWasmPoolCodeAndWhiteListProposal, - }, -}; -use osmosis_test_tube::{Account, GovWithAppAccess, Module, OsmosisTestApp, Runner}; -use rstest::rstest; - -#[cw_serde] -struct InstantiateMsgV2 { - pool_asset_denoms: Vec, - alloyed_asset_subdenom: String, - admin: Option, - moderator: Option, -} - -#[cw_serde] -struct MigrateMsgV3 { - asset_configs: Vec, - alloyed_asset_normalization_factor: Uint128, - moderator: Option, -} - -#[test] -fn test_migrate_v2_to_v3() { - // --- setup account --- - let app = OsmosisTestApp::new(); - let signer = app - .init_account(&[ - Coin::new(100000, "denom1"), - Coin::new(100000, "denom2"), - Coin::new(10000000000000, "uosmo"), - ]) - .unwrap(); - - // --- create pool ---- - - let cp = CosmwasmPool::new(&app); - let gov = GovWithAppAccess::new(&app); - gov.propose_and_execute( - UploadCosmWasmPoolCodeAndWhiteListProposal::TYPE_URL.to_string(), - UploadCosmWasmPoolCodeAndWhiteListProposal { - title: String::from("store test cosmwasm pool code"), - description: String::from("test"), - wasm_byte_code: get_prev_version_of_wasm_byte_code("v2"), - }, - signer.address(), - &signer, - ) - .unwrap(); - - let instantiate_msg = InstantiateMsgV2 { - pool_asset_denoms: vec!["denom1".to_string(), "denom2".to_string()], - alloyed_asset_subdenom: "denomx".to_string(), - admin: Some(signer.address()), - moderator: None, - }; - - let code_id = 1; - let res = cp - .create_cosmwasm_pool( - MsgCreateCosmWasmPool { - code_id, - instantiate_msg: to_json_binary(&instantiate_msg).unwrap().to_vec(), - sender: signer.address(), - }, - &signer, - ) - .unwrap(); - - let pool_id = res.data.pool_id; - - let ContractInfoByPoolIdResponse { - contract_address, - code_id: _, - } = cp - .contract_info_by_pool_id(&ContractInfoByPoolIdRequest { pool_id }) - .unwrap(); - - let t = TransmuterContract::new(&app, code_id, pool_id, contract_address.clone()); - - // --- migrate pool --- - let migrate_msg = MigrateMsgV3 { - asset_configs: vec![ - AssetConfig { - denom: "denom1".to_string(), - normalization_factor: Uint128::new(1), - }, - AssetConfig { - denom: "denom2".to_string(), - normalization_factor: Uint128::new(10000), - }, - ], - alloyed_asset_normalization_factor: Uint128::new(10), - moderator: Some("osmo1cyyzpxplxdzkeea7kwsydadg87357qnahakaks".to_string()), - }; - - gov.propose_and_execute( - MigratePoolContractsProposal::TYPE_URL.to_string(), - MigratePoolContractsProposal { - title: "migrate cosmwasmpool".to_string(), - description: "test migration".to_string(), - pool_ids: vec![pool_id], - new_code_id: 0, // upload new code, so set this to 0 - wasm_byte_code: get_prev_version_of_wasm_byte_code("v3"), - migrate_msg: to_json_binary(&migrate_msg).unwrap().to_vec(), - }, - signer.address(), - &signer, - ) - .unwrap(); - - let alloyed_denom = format!("factory/{contract_address}/alloyed/denomx"); - - let expected_asset_configs = migrate_msg - .asset_configs - .into_iter() - .chain(iter::once(AssetConfig { - denom: alloyed_denom, - normalization_factor: migrate_msg.alloyed_asset_normalization_factor, - })) - .collect::>(); - - // list asset configs - let ListAssetConfigsResponse { asset_configs } = - t.query(&QueryMsg::ListAssetConfigs {}).unwrap(); - - assert_eq!(asset_configs, expected_asset_configs); - - let GetModeratorResponse { moderator } = t.query(&QueryMsg::GetModerator {}).unwrap(); - assert_eq!(moderator, migrate_msg.moderator.unwrap()); -} - -#[rstest] -#[case("v3")] -#[case("v3_1")] -fn test_migrate_v3(#[case] from_version: &str) { - // --- setup account --- - let app = OsmosisTestApp::new(); - let signer = app - .init_account(&[ - Coin::new(100000, "denom1"), - Coin::new(100000, "denom2"), - Coin::new(10000000000000, "uosmo"), - ]) - .unwrap(); - - // --- create pool ---- - - let cp = CosmwasmPool::new(&app); - let gov = GovWithAppAccess::new(&app); - gov.propose_and_execute( - UploadCosmWasmPoolCodeAndWhiteListProposal::TYPE_URL.to_string(), - UploadCosmWasmPoolCodeAndWhiteListProposal { - title: String::from("store test cosmwasm pool code"), - description: String::from("test"), - wasm_byte_code: get_prev_version_of_wasm_byte_code(from_version), - }, - signer.address(), - &signer, - ) - .unwrap(); - - let instantiate_msg = InstantiateMsg { - pool_asset_configs: vec![ - AssetConfig { - denom: "denom1".to_string(), - normalization_factor: Uint128::new(1), - }, - AssetConfig { - denom: "denom2".to_string(), - normalization_factor: Uint128::new(10000), - }, - ], - alloyed_asset_subdenom: "denomx".to_string(), - alloyed_asset_normalization_factor: Uint128::new(10), - admin: Some(signer.address()), - moderator: signer.address(), - }; - - let code_id = 1; - let res = cp - .create_cosmwasm_pool( - MsgCreateCosmWasmPool { - code_id, - instantiate_msg: to_json_binary(&instantiate_msg).unwrap().to_vec(), - sender: signer.address(), - }, - &signer, - ) - .unwrap(); - - let pool_id = res.data.pool_id; - - let ContractInfoByPoolIdResponse { - contract_address, - code_id: _, - } = cp - .contract_info_by_pool_id(&ContractInfoByPoolIdRequest { pool_id }) - .unwrap(); - - let t = TransmuterContract::new(&app, code_id, pool_id, contract_address.clone()); - - // --- migrate pool --- - let migrate_msg = MigrateMsg {}; - - gov.propose_and_execute( - MigratePoolContractsProposal::TYPE_URL.to_string(), - MigratePoolContractsProposal { - title: "migrate cosmwasmpool".to_string(), - description: "test migration".to_string(), - pool_ids: vec![pool_id], - new_code_id: 0, // upload new code, so set this to 0 - wasm_byte_code: TransmuterContract::get_wasm_byte_code(), - migrate_msg: to_json_binary(&migrate_msg).unwrap().to_vec(), - }, - signer.address(), - &signer, - ) - .unwrap(); - - let alloyed_denom = format!("factory/{contract_address}/alloyed/denomx"); - - let expected_asset_configs = instantiate_msg - .pool_asset_configs - .into_iter() - .chain(iter::once(AssetConfig { - denom: alloyed_denom, - normalization_factor: instantiate_msg.alloyed_asset_normalization_factor, - })) - .collect::>(); - - // list asset configs - let ListAssetConfigsResponse { asset_configs } = - t.query(&QueryMsg::ListAssetConfigs {}).unwrap(); - - // expect no changes in asset config - assert_eq!(asset_configs, expected_asset_configs); - - let res: QueryRawContractStateResponse = app - .query( - "/cosmwasm.wasm.v1.Query/RawContractState", - &QueryRawContractStateRequest { - address: t.contract_addr, - query_data: b"contract_info".to_vec(), - }, - ) - .unwrap(); - - let version: cw2::ContractVersion = from_json(res.data).unwrap(); - - assert_eq!( - version, - cw2::ContractVersion { - contract: "crates.io:transmuter".to_string(), - version: "3.2.0".to_string() - } - ); -} - -fn get_prev_version_of_wasm_byte_code(version: &str) -> Vec { - let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let wasm_path = manifest_path - .join("testdata") - .join(format!("transmuter_{version}.wasm")); - - let err_msg = &format!("failed to read wasm file: {}", wasm_path.display()); - std::fs::read(wasm_path).expect(err_msg) -} +// use std::{iter, path::PathBuf}; + +// use crate::{ +// asset::AssetConfig, +// contract::{ +// sv::{InstantiateMsg, QueryMsg}, +// GetModeratorResponse, ListAssetConfigsResponse, +// }, +// migrations::v3_2_0::MigrateMsg, +// test::{modules::cosmwasm_pool::CosmwasmPool, test_env::TransmuterContract}, +// }; +// use cosmwasm_schema::cw_serde; +// use cosmwasm_std::{from_json, to_json_binary, Coin, Uint128}; +// use osmosis_std::types::{ +// cosmwasm::wasm::v1::{QueryRawContractStateRequest, QueryRawContractStateResponse}, +// osmosis::cosmwasmpool::v1beta1::{ +// ContractInfoByPoolIdRequest, ContractInfoByPoolIdResponse, MigratePoolContractsProposal, +// MsgCreateCosmWasmPool, UploadCosmWasmPoolCodeAndWhiteListProposal, +// }, +// }; +// use osmosis_test_tube::{Account, GovWithAppAccess, Module, OsmosisTestApp, Runner}; +// use rstest::rstest; + +// #[cw_serde] +// struct InstantiateMsgV2 { +// pool_asset_denoms: Vec, +// alloyed_asset_subdenom: String, +// admin: Option, +// moderator: Option, +// } + +// #[cw_serde] +// struct MigrateMsgV3 { +// asset_configs: Vec, +// alloyed_asset_normalization_factor: Uint128, +// moderator: Option, +// } + +// #[test] +// fn test_migrate_v2_to_v3() { +// // --- setup account --- +// let app = OsmosisTestApp::new(); +// let signer = app +// .init_account(&[ +// Coin::new(100000, "denom1"), +// Coin::new(100000, "denom2"), +// Coin::new(10000000000000, "uosmo"), +// ]) +// .unwrap(); + +// // --- create pool ---- + +// let cp = CosmwasmPool::new(&app); +// let gov = GovWithAppAccess::new(&app); +// gov.propose_and_execute( +// UploadCosmWasmPoolCodeAndWhiteListProposal::TYPE_URL.to_string(), +// UploadCosmWasmPoolCodeAndWhiteListProposal { +// title: String::from("store test cosmwasm pool code"), +// description: String::from("test"), +// wasm_byte_code: get_prev_version_of_wasm_byte_code("v2"), +// }, +// signer.address(), +// &signer, +// ) +// .unwrap(); + +// let instantiate_msg = InstantiateMsgV2 { +// pool_asset_denoms: vec!["denom1".to_string(), "denom2".to_string()], +// alloyed_asset_subdenom: "denomx".to_string(), +// admin: Some(signer.address()), +// moderator: None, +// }; + +// let code_id = 1; +// let res = cp +// .create_cosmwasm_pool( +// MsgCreateCosmWasmPool { +// code_id, +// instantiate_msg: to_json_binary(&instantiate_msg).unwrap().to_vec(), +// sender: signer.address(), +// }, +// &signer, +// ) +// .unwrap(); + +// let pool_id = res.data.pool_id; + +// let ContractInfoByPoolIdResponse { +// contract_address, +// code_id: _, +// } = cp +// .contract_info_by_pool_id(&ContractInfoByPoolIdRequest { pool_id }) +// .unwrap(); + +// let t = TransmuterContract::new(&app, code_id, pool_id, contract_address.clone()); + +// // --- migrate pool --- +// let migrate_msg = MigrateMsgV3 { +// asset_configs: vec![ +// AssetConfig { +// denom: "denom1".to_string(), +// normalization_factor: Uint128::new(1), +// }, +// AssetConfig { +// denom: "denom2".to_string(), +// normalization_factor: Uint128::new(10000), +// }, +// ], +// alloyed_asset_normalization_factor: Uint128::new(10), +// moderator: Some("osmo1cyyzpxplxdzkeea7kwsydadg87357qnahakaks".to_string()), +// }; + +// gov.propose_and_execute( +// MigratePoolContractsProposal::TYPE_URL.to_string(), +// MigratePoolContractsProposal { +// title: "migrate cosmwasmpool".to_string(), +// description: "test migration".to_string(), +// pool_ids: vec![pool_id], +// new_code_id: 0, // upload new code, so set this to 0 +// wasm_byte_code: get_prev_version_of_wasm_byte_code("v3"), +// migrate_msg: to_json_binary(&migrate_msg).unwrap().to_vec(), +// }, +// signer.address(), +// &signer, +// ) +// .unwrap(); + +// let alloyed_denom = format!("factory/{contract_address}/alloyed/denomx"); + +// let expected_asset_configs = migrate_msg +// .asset_configs +// .into_iter() +// .chain(iter::once(AssetConfig { +// denom: alloyed_denom, +// normalization_factor: migrate_msg.alloyed_asset_normalization_factor, +// })) +// .collect::>(); + +// // list asset configs +// let ListAssetConfigsResponse { asset_configs } = +// t.query(&QueryMsg::ListAssetConfigs {}).unwrap(); + +// assert_eq!(asset_configs, expected_asset_configs); + +// let GetModeratorResponse { moderator } = t.query(&QueryMsg::GetModerator {}).unwrap(); +// assert_eq!(moderator, migrate_msg.moderator.unwrap()); +// } + +// #[rstest] +// #[case("v3")] +// #[case("v3_1")] +// fn test_migrate_v3(#[case] from_version: &str) { +// // --- setup account --- +// let app = OsmosisTestApp::new(); +// let signer = app +// .init_account(&[ +// Coin::new(100000, "denom1"), +// Coin::new(100000, "denom2"), +// Coin::new(10000000000000, "uosmo"), +// ]) +// .unwrap(); + +// // --- create pool ---- + +// let cp = CosmwasmPool::new(&app); +// let gov = GovWithAppAccess::new(&app); +// gov.propose_and_execute( +// UploadCosmWasmPoolCodeAndWhiteListProposal::TYPE_URL.to_string(), +// UploadCosmWasmPoolCodeAndWhiteListProposal { +// title: String::from("store test cosmwasm pool code"), +// description: String::from("test"), +// wasm_byte_code: get_prev_version_of_wasm_byte_code(from_version), +// }, +// signer.address(), +// &signer, +// ) +// .unwrap(); + +// let instantiate_msg = InstantiateMsg { +// pool_asset_configs: vec![ +// AssetConfig { +// denom: "denom1".to_string(), +// normalization_factor: Uint128::new(1), +// }, +// AssetConfig { +// denom: "denom2".to_string(), +// normalization_factor: Uint128::new(10000), +// }, +// ], +// alloyed_asset_subdenom: "denomx".to_string(), +// alloyed_asset_normalization_factor: Uint128::new(10), +// admin: Some(signer.address()), +// moderator: signer.address(), +// }; + +// let code_id = 1; +// let res = cp +// .create_cosmwasm_pool( +// MsgCreateCosmWasmPool { +// code_id, +// instantiate_msg: to_json_binary(&instantiate_msg).unwrap().to_vec(), +// sender: signer.address(), +// }, +// &signer, +// ) +// .unwrap(); + +// let pool_id = res.data.pool_id; + +// let ContractInfoByPoolIdResponse { +// contract_address, +// code_id: _, +// } = cp +// .contract_info_by_pool_id(&ContractInfoByPoolIdRequest { pool_id }) +// .unwrap(); + +// let t = TransmuterContract::new(&app, code_id, pool_id, contract_address.clone()); + +// // --- migrate pool --- +// let migrate_msg = MigrateMsg {}; + +// gov.propose_and_execute( +// MigratePoolContractsProposal::TYPE_URL.to_string(), +// MigratePoolContractsProposal { +// title: "migrate cosmwasmpool".to_string(), +// description: "test migration".to_string(), +// pool_ids: vec![pool_id], +// new_code_id: 0, // upload new code, so set this to 0 +// wasm_byte_code: TransmuterContract::get_wasm_byte_code(), +// migrate_msg: to_json_binary(&migrate_msg).unwrap().to_vec(), +// }, +// signer.address(), +// &signer, +// ) +// .unwrap(); + +// let alloyed_denom = format!("factory/{contract_address}/alloyed/denomx"); + +// let expected_asset_configs = instantiate_msg +// .pool_asset_configs +// .into_iter() +// .chain(iter::once(AssetConfig { +// denom: alloyed_denom, +// normalization_factor: instantiate_msg.alloyed_asset_normalization_factor, +// })) +// .collect::>(); + +// // list asset configs +// let ListAssetConfigsResponse { asset_configs } = +// t.query(&QueryMsg::ListAssetConfigs {}).unwrap(); + +// // expect no changes in asset config +// assert_eq!(asset_configs, expected_asset_configs); + +// let res: QueryRawContractStateResponse = app +// .query( +// "/cosmwasm.wasm.v1.Query/RawContractState", +// &QueryRawContractStateRequest { +// address: t.contract_addr, +// query_data: b"contract_info".to_vec(), +// }, +// ) +// .unwrap(); + +// let version: cw2::ContractVersion = from_json(res.data).unwrap(); + +// assert_eq!( +// version, +// cw2::ContractVersion { +// contract: "crates.io:transmuter".to_string(), +// version: "3.2.0".to_string() +// } +// ); +// } + +// fn get_prev_version_of_wasm_byte_code(version: &str) -> Vec { +// let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); +// let wasm_path = manifest_path +// .join("testdata") +// .join(format!("transmuter_{version}.wasm")); + +// let err_msg = &format!("failed to read wasm file: {}", wasm_path.display()); +// std::fs::read(wasm_path).expect(err_msg) +// } From ab50f6c400917d741062396df0b1a0e94e8e944e Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Wed, 2 Oct 2024 16:00:56 +0700 Subject: [PATCH 07/26] asset group weight --- contracts/transmuter/src/contract.rs | 2 +- contracts/transmuter/src/limiter.rs | 2 +- contracts/transmuter/src/swap.rs | 16 ++--- .../src/transmuter_pool/asset_group.rs | 67 ++++++++++++++++++- .../src/transmuter_pool/corrupted_assets.rs | 10 ++- .../transmuter/src/transmuter_pool/weight.rs | 12 ++-- 6 files changed, 91 insertions(+), 18 deletions(-) diff --git a/contracts/transmuter/src/contract.rs b/contracts/transmuter/src/contract.rs index a2edb90..114b918 100644 --- a/contracts/transmuter/src/contract.rs +++ b/contracts/transmuter/src/contract.rs @@ -230,7 +230,7 @@ impl Transmuter<'_> { self.limiters.reset_change_limiter_states( deps.storage, env.block.time, - pool.weights()? + pool.asset_weights()? .unwrap_or_default() .into_iter() .map(|(denom, weight)| (Scope::denom(&denom).key(), weight)) // TODO: handle asset group diff --git a/contracts/transmuter/src/limiter.rs b/contracts/transmuter/src/limiter.rs index c1dd7d8..38c6e60 100644 --- a/contracts/transmuter/src/limiter.rs +++ b/contracts/transmuter/src/limiter.rs @@ -621,7 +621,7 @@ macro_rules! assert_reset_change_limiters_by_scope { ($scope:expr, $reset_at:expr, $transmuter:expr, $storage:expr) => { let pool = $transmuter.pool.load($storage).unwrap(); let weights = pool - .weights() + .asset_weights() .unwrap() .unwrap_or_default() .into_iter() diff --git a/contracts/transmuter/src/swap.rs b/contracts/transmuter/src/swap.rs index 84dafcf..f6de56c 100644 --- a/contracts/transmuter/src/swap.rs +++ b/contracts/transmuter/src/swap.rs @@ -130,7 +130,7 @@ impl Transmuter<'_> { pool.join_pool(&tokens_in)?; // check and update limiters only if pool assets are not zero - if let Some(updated_weights) = pool.weights()? { + if let Some(updated_weights) = pool.asset_weights()? { let scope_value_pairs = construct_scope_value_pairs( prev_weights, updated_weights, @@ -300,7 +300,7 @@ impl Transmuter<'_> { self.limiters.reset_change_limiter_states( deps.storage, env.block.time, - pool.weights()? + pool.asset_weights()? .unwrap_or_default() .into_iter() .map(|(denom, weight)| (Scope::denom(&denom).key(), weight)) // TODO: handle asset group @@ -312,7 +312,7 @@ impl Transmuter<'_> { pool.exit_pool(&tokens_out)?; // check and update limiters only if pool assets are not zero - if let Some(updated_weights) = pool.weights()? { + if let Some(updated_weights) = pool.asset_weights()? { let scope_value_pairs = construct_scope_value_pairs( prev_weights, updated_weights, @@ -377,7 +377,7 @@ impl Transmuter<'_> { ); // check and update limiters only if pool assets are not zero - if let Some(updated_weights) = pool.weights()? { + if let Some(updated_weights) = pool.asset_weights()? { let scope_value_pairs = construct_scope_value_pairs( prev_weights, updated_weights, @@ -438,7 +438,7 @@ impl Transmuter<'_> { ); // check and update limiters only if pool assets are not zero - if let Some(updated_weights) = pool.weights()? { + if let Some(updated_weights) = pool.asset_weights()? { let scope_value_pairs = construct_scope_value_pairs( prev_weights, updated_weights, @@ -620,12 +620,12 @@ impl Transmuter<'_> { storage, Scope::denom(corrupted.denom()), // TODO: bubble this up )?; - - // TODO: remove limiters from asset group too - // TODO: remove denom from asset group, if asset group is empty, remove it } } + // TODO: remove limiters from asset group too + // TODO: remove denom from asset group, if asset group is empty, remove it + Ok(()) } } diff --git a/contracts/transmuter/src/transmuter_pool/asset_group.rs b/contracts/transmuter/src/transmuter_pool/asset_group.rs index 09190b3..17c546d 100644 --- a/contracts/transmuter/src/transmuter_pool/asset_group.rs +++ b/contracts/transmuter/src/transmuter_pool/asset_group.rs @@ -1,5 +1,7 @@ +use std::collections::BTreeMap; + use cosmwasm_schema::cw_serde; -use cosmwasm_std::ensure; +use cosmwasm_std::{ensure, Decimal}; use crate::{corruptable::Corruptable, ContractError}; @@ -107,6 +109,8 @@ impl TransmuterPool { ); } + // TODO: limit sizes of asset groups + self.asset_groups.insert(label, AssetGroup::new(denoms)); Ok(self) @@ -122,10 +126,37 @@ impl TransmuterPool { Ok(self) } + + pub fn asset_group_weights(&self) -> Result, ContractError> { + let denom_weights: BTreeMap<_, _> = self + .asset_weights()? + .unwrap_or_default() + .into_iter() + .collect(); + + let mut weights = BTreeMap::new(); + for (label, asset_group) in &self.asset_groups { + let mut group_weight = Decimal::zero(); + for denom in &asset_group.denoms { + let denom_weight = denom_weights + .get(denom) + .copied() + .unwrap_or_else(Decimal::zero); + group_weight = group_weight.checked_add(denom_weight)?; + } + weights.insert(label.to_string(), group_weight); + } + + Ok(weights) + } } #[cfg(test)] mod tests { + use cosmwasm_std::Uint128; + + use crate::asset::Asset; + use super::*; #[test] @@ -176,4 +207,38 @@ mod tests { group.unmark_as_corrupted().unmark_as_corrupted(); assert!(!group.is_corrupted()); } + + #[test] + fn test_asset_group_weights() { + let mut pool = TransmuterPool::new(vec![ + Asset::new(Uint128::new(200), "denom1", Uint128::new(2)).unwrap(), + Asset::new(Uint128::new(300), "denom2", Uint128::new(3)).unwrap(), + Asset::new(Uint128::new(500), "denom3", Uint128::new(5)).unwrap(), + ]) + .unwrap(); + + // Test with empty pool + let weights = pool.asset_group_weights().unwrap(); + assert!(weights.is_empty()); + + pool.create_asset_group( + "group1".to_string(), + vec!["denom1".to_string(), "denom2".to_string()], + ) + .unwrap(); + + pool.create_asset_group("group2".to_string(), vec!["denom3".to_string()]) + .unwrap(); + + let weights = pool.asset_group_weights().unwrap(); + assert_eq!(weights.len(), 2); + assert_eq!( + weights.get("group1").unwrap(), + &Decimal::raw(666666666666666666) + ); + assert_eq!( + weights.get("group2").unwrap(), + &Decimal::raw(333333333333333333) + ); + } } diff --git a/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs b/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs index 9b8f095..6f502f9 100644 --- a/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs +++ b/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs @@ -96,7 +96,7 @@ impl TransmuterPool { } // if total pool value == 0 -> Empty mapping, later unwrap weight will be 0 - let weight_pre_action = self.weights()?.unwrap_or_default(); + let weight_pre_action = self.asset_weights()?.unwrap_or_default(); let weight_pre_action = weight_pre_action.into_iter().collect::>(); let res = action(self)?; @@ -108,7 +108,7 @@ impl TransmuterPool { .filter(|asset| asset.is_corrupted()); // if total pool value == 0 -> Empty mapping, later unwrap weight will be 0 - let weight_post_action = self.weights()?.unwrap_or_default(); + let weight_post_action = self.asset_weights()?.unwrap_or_default(); let weight_post_action = weight_post_action.into_iter().collect::>(); for post_action in corrupted_assets_post_action { @@ -198,7 +198,11 @@ impl TransmuterPool { } fn get_weights(&self) -> Result, ContractError> { - Ok(self.weights()?.unwrap_or_default().into_iter().collect()) + Ok(self + .asset_weights()? + .unwrap_or_default() + .into_iter() + .collect()) } /// Get the state of corrupted asset groups. diff --git a/contracts/transmuter/src/transmuter_pool/weight.rs b/contracts/transmuter/src/transmuter_pool/weight.rs index 08d53db..4992cdf 100644 --- a/contracts/transmuter/src/transmuter_pool/weight.rs +++ b/contracts/transmuter/src/transmuter_pool/weight.rs @@ -20,7 +20,7 @@ impl TransmuterPool { /// /// If total pool asset amount is zero, returns None to signify that /// it makes no sense to calculate ratios, but not an error. - pub fn weights(&self) -> Result>, ContractError> { + pub fn asset_weights(&self) -> Result>, ContractError> { let std_norm_factor = lcm_from_iter( self.pool_assets .iter() @@ -52,7 +52,11 @@ impl TransmuterPool { } pub fn weights_map(&self) -> Result, ContractError> { - Ok(self.weights()?.unwrap_or_default().into_iter().collect()) + Ok(self + .asset_weights()? + .unwrap_or_default() + .into_iter() + .collect()) } fn normalized_asset_values( @@ -176,7 +180,7 @@ mod tests { asset_groups: BTreeMap::new(), }; - let ratios = pool.weights().unwrap(); + let ratios = pool.asset_weights().unwrap(); assert_eq!(ratios, Some(expected)); } @@ -190,7 +194,7 @@ mod tests { asset_groups: BTreeMap::new(), }; - let ratios = pool.weights().unwrap(); + let ratios = pool.asset_weights().unwrap(); assert_eq!(ratios, None); } } From e1012025df59c2e98a83383c9c91d825249a773c Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Wed, 2 Oct 2024 16:23:57 +0700 Subject: [PATCH 08/26] remove corrupted asset also removes ref from asset group --- .../src/transmuter_pool/corrupted_assets.rs | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs b/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs index 6f502f9..aca5728 100644 --- a/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs +++ b/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs @@ -74,6 +74,17 @@ impl TransmuterPool { ); self.pool_assets.retain(|asset| asset.denom() != denom); + + // remove corrupted asset from asset groups + for label in self.asset_groups.clone().keys() { + let asset_group = self.asset_groups.get_mut(label).unwrap(); + asset_group.remove_denoms(vec![denom.to_string()]); + + if asset_group.denoms().is_empty() { + self.asset_groups.remove(label); + } + } + Ok(()) } @@ -614,4 +625,98 @@ mod tests { } ); } + + #[test] + fn test_remove_corrupted_asset() { + let mut pool = TransmuterPool { + pool_assets: Asset::unchecked_equal_assets_from_coins(&[ + Coin::new(100, "asset1"), + Coin::new(200, "asset2"), + Coin::new(300, "asset3"), + ]), + asset_groups: BTreeMap::from_iter(vec![( + "group1".to_string(), + AssetGroup::new(vec!["asset1".to_string(), "asset2".to_string()]), + )]), + }; + + // Mark asset2 as corrupted + pool.mark_corrupted_asset("asset2").unwrap(); + + // Attempt to remove asset2 with non-zero amount (should fail) + let err = pool.remove_corrupted_asset("asset2").unwrap_err(); + assert_eq!(err, ContractError::InvalidCorruptedAssetRemoval {}); + + // Decrease amount of asset2 to zero + for asset in pool.pool_assets.iter_mut() { + if asset.denom() == "asset2" { + asset.decrease_amount(Uint128::new(200)).unwrap(); + } + } + + // Remove corrupted asset2 (should succeed) + pool.remove_corrupted_asset("asset2").unwrap(); + + assert_eq!( + pool.asset_groups, + BTreeMap::from_iter(vec![( + "group1".to_string(), + AssetGroup::new(vec!["asset1".to_string()]), + )]) + ); + + // Verify asset2 is removed + assert_eq!( + pool.pool_assets, + vec![ + Asset::unchecked(Uint128::new(100), "asset1", Uint128::one()), + Asset::unchecked(Uint128::new(300), "asset3", Uint128::one()), + ] + ); + + // Attempt to remove non-corrupted asset (should fail) + let err = pool.remove_corrupted_asset("asset1").unwrap_err(); + assert_eq!( + err, + ContractError::InvalidCorruptedAssetDenom { + denom: "asset1".to_string() + } + ); + + // Attempt to remove non-existent asset (should fail) + let err = pool.remove_corrupted_asset("non_existent").unwrap_err(); + assert_eq!( + err, + ContractError::InvalidTransmuteDenom { + denom: "non_existent".to_string(), + expected_denom: vec!["asset1".to_string(), "asset3".to_string()] + } + ); + + // Mark asset1 as corrupted + pool.mark_corrupted_asset("asset1").unwrap(); + + // Decrease amount of asset1 to zero + for asset in pool.pool_assets.iter_mut() { + if asset.denom() == "asset1" { + asset.decrease_amount(Uint128::new(100)).unwrap(); + } + } + + // Remove corrupted asset1 + pool.remove_corrupted_asset("asset1").unwrap(); + + // Verify asset1 is removed + assert_eq!( + pool.pool_assets, + vec![Asset::unchecked( + Uint128::new(300), + "asset3", + Uint128::one() + ),] + ); + + // Verify asset groups are updated + assert!(pool.asset_groups.is_empty()); + } } From 1283470bac4274092ee66f3ccd57403bc16a9d86 Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Thu, 3 Oct 2024 16:45:53 +0700 Subject: [PATCH 09/26] cascade remove drained corrupted asset group --- contracts/transmuter/src/error.rs | 2 +- contracts/transmuter/src/swap.rs | 226 +++++++++++++++++- .../src/transmuter_pool/corrupted_assets.rs | 32 +-- 3 files changed, 227 insertions(+), 33 deletions(-) diff --git a/contracts/transmuter/src/error.rs b/contracts/transmuter/src/error.rs index b15ac81..c01beb1 100644 --- a/contracts/transmuter/src/error.rs +++ b/contracts/transmuter/src/error.rs @@ -48,7 +48,7 @@ pub enum ContractError { InvalidCorruptedAssetDenom { denom: String }, #[error("Only corrupted asset with 0 amount can be removed")] - InvalidCorruptedAssetRemoval {}, + InvalidAssetRemoval {}, #[error("Pool asset denom count must be within {min} - {max} inclusive, but got: {actual}")] PoolAssetDenomCountOutOfRange { diff --git a/contracts/transmuter/src/swap.rs b/contracts/transmuter/src/swap.rs index f6de56c..950741d 100644 --- a/contracts/transmuter/src/swap.rs +++ b/contracts/transmuter/src/swap.rs @@ -11,6 +11,7 @@ use serde::Serialize; use crate::{ alloyed_asset::{swap_from_alloyed, swap_to_alloyed}, contract::Transmuter, + corruptable::Corruptable, scope::Scope, transmuter_pool::{AmountConstraint, AssetGroup, TransmuterPool}, ContractError, @@ -613,18 +614,35 @@ impl Transmuter<'_> { storage: &mut dyn Storage, pool: &mut TransmuterPool, ) -> Result<(), ContractError> { + // remove corrupted assets for corrupted in pool.clone().corrupted_assets() { if corrupted.amount().is_zero() { - pool.remove_corrupted_asset(corrupted.denom())?; - self.limiters.uncheck_deregister_all_for_scope( - storage, - Scope::denom(corrupted.denom()), // TODO: bubble this up - )?; + pool.remove_asset(corrupted.denom())?; + self.limiters + .uncheck_deregister_all_for_scope(storage, Scope::denom(corrupted.denom()))?; } } - // TODO: remove limiters from asset group too - // TODO: remove denom from asset group, if asset group is empty, remove it + // remove assets from asset groups + for (label, asset_group) in pool.clone().asset_groups { + // if asset group is corrupted + if asset_group.is_corrupted() { + // remove asset from pool if amount is zero. + // removing asset here will also remove it from the asset group + for denom in asset_group.denoms() { + if pool.get_pool_asset_by_denom(denom)?.amount().is_zero() { + pool.remove_asset(denom)?; + } + } + + // remove asset group is removed + // remove limiters for asset group as well + if pool.asset_groups.get(&label).is_none() { + self.limiters + .uncheck_deregister_all_for_scope(storage, Scope::asset_group(&label))?; + } + } + } Ok(()) } @@ -760,7 +778,7 @@ pub enum BurnTarget { #[cfg(test)] mod tests { - use crate::{asset::Asset, limiter::LimiterParams}; + use crate::{asset::Asset, corruptable::Corruptable, limiter::LimiterParams}; use super::*; use cosmwasm_std::{ @@ -1754,4 +1772,196 @@ mod tests { assert_eq!(scope_value_pairs, expected_scope_value_pairs); } + + #[test] + fn test_clean_up_drained_corrupted_assets_group() { + let sender = Addr::unchecked("addr1"); + let mut deps = cosmwasm_std::testing::mock_dependencies_with_balances(&[( + sender.to_string().as_str(), + &[Coin::new(2000000000000u128, "alloyed")], + )]); + + let transmuter = Transmuter::default(); + transmuter + .alloyed_asset + .set_alloyed_denom(&mut deps.storage, &"alloyed".to_string()) + .unwrap(); + + transmuter + .alloyed_asset + .set_normalization_factor(&mut deps.storage, 100u128.into()) + .unwrap(); + + let init_pool = TransmuterPool { + pool_assets: vec![ + Asset::new(Uint128::from(1000000000000u128), "denom1", 1u128).unwrap(), + Asset::new(Uint128::from(1000000000000u128), "denom2", 10u128).unwrap(), + Asset::new(Uint128::from(1000000000000u128), "denom3", 100u128).unwrap(), + ], + asset_groups: BTreeMap::from([( + "group1".to_string(), + AssetGroup::new(vec!["denom2".to_string(), "denom3".to_string()]) + .mark_as_corrupted() + .clone(), + )]), + }; + transmuter.pool.save(&mut deps.storage, &init_pool).unwrap(); + + // Register limiters for group1 + transmuter + .limiters + .register( + &mut deps.storage, + Scope::asset_group("group1"), + "1w", + LimiterParams::StaticLimiter { + upper_limit: Decimal::percent(60), + }, + ) + .unwrap(); + + let mut pool = transmuter.pool.load(&deps.storage).unwrap(); + let res = transmuter.clean_up_drained_corrupted_assets(&mut deps.storage, &mut pool); + assert_eq!(res, Ok(())); + + pool = transmuter.pool.load(&deps.storage).unwrap(); + assert_eq!(pool, init_pool); + + pool.exit_pool(&[Coin::new(1000000000000u128, "denom2")]) + .unwrap(); + transmuter.pool.save(&mut deps.storage, &pool).unwrap(); + + let res = transmuter.clean_up_drained_corrupted_assets(&mut deps.storage, &mut pool); + assert_eq!(res, Ok(())); + + let expected_pool = TransmuterPool { + pool_assets: vec![ + Asset::new(Uint128::from(1000000000000u128), "denom1", 1u128).unwrap(), + Asset::new(Uint128::from(1000000000000u128), "denom3", 100u128).unwrap(), + ], + asset_groups: BTreeMap::from([( + "group1".to_string(), + AssetGroup::new(vec!["denom3".to_string()]) + .mark_as_corrupted() + .clone(), + )]), + }; + assert_eq!(pool, expected_pool); + + // Check that the limiter for group1 is still registered + let limiters = transmuter.limiters.list_limiters(&deps.storage).unwrap(); + assert_eq!(limiters.len(), 1); + + // Save the updated pool + transmuter.pool.save(&mut deps.storage, &pool).unwrap(); + + pool.exit_pool(&[Coin::new(1000000000000u128, "denom3")]) + .unwrap(); + + let res = transmuter.clean_up_drained_corrupted_assets(&mut deps.storage, &mut pool); + assert_eq!(res, Ok(())); + + let expected_pool = TransmuterPool { + pool_assets: vec![ + Asset::new(Uint128::from(1000000000000u128), "denom1", 1u128).unwrap(), + ], + asset_groups: BTreeMap::new(), + }; + assert_eq!(pool, expected_pool); + + // Check that the limiter for group1 is removed + let limiters = transmuter.limiters.list_limiters(&deps.storage).unwrap(); + assert_eq!(limiters.len(), 0); + } + + #[test] + fn test_clean_up_drained_corrupted_assets_group_not_corrupted() { + let mut deps = mock_dependencies(); + let transmuter = Transmuter::default(); + + // Initialize the pool with non-corrupted assets and groups + let init_pool = TransmuterPool { + pool_assets: vec![ + Asset::new(Uint128::from(1000000000000u128), "denom1", 1u128).unwrap(), + Asset::new(Uint128::from(1000000000000u128), "denom2", 10u128).unwrap(), + Asset::new(Uint128::from(1000000000000u128), "denom3", 100u128).unwrap(), + ], + asset_groups: BTreeMap::from([( + "group1".to_string(), + AssetGroup::new(vec!["denom2".to_string(), "denom3".to_string()]), + )]), + }; + + transmuter.pool.save(&mut deps.storage, &init_pool).unwrap(); + + // Register a limiter for the group + transmuter + .limiters + .register( + &mut deps.storage, + Scope::asset_group("group1"), + "limiter1", + LimiterParams::StaticLimiter { + upper_limit: Decimal::one(), + }, + ) + .unwrap(); + + let mut pool = transmuter.pool.load(&deps.storage).unwrap(); + assert_eq!(pool, init_pool); + + // Drain denom2 from the pool + pool.exit_pool(&[Coin::new(1000000000000u128, "denom2")]) + .unwrap(); + transmuter.pool.save(&mut deps.storage, &pool).unwrap(); + + let res = transmuter.clean_up_drained_corrupted_assets(&mut deps.storage, &mut pool); + assert_eq!(res, Ok(())); + + // Check that the pool remains unchanged + let expected_pool = TransmuterPool { + pool_assets: vec![ + Asset::new(Uint128::from(1000000000000u128), "denom1", 1u128).unwrap(), + Asset::new(Uint128::zero(), "denom2", 10u128).unwrap(), + Asset::new(Uint128::from(1000000000000u128), "denom3", 100u128).unwrap(), + ], + asset_groups: BTreeMap::from([( + "group1".to_string(), + AssetGroup::new(vec!["denom2".to_string(), "denom3".to_string()]), + )]), + }; + assert_eq!(pool, expected_pool); + + // Check that the limiter for group1 is still registered + let limiters = transmuter.limiters.list_limiters(&deps.storage).unwrap(); + assert_eq!(limiters.len(), 1); + + // Save the updated pool + transmuter.pool.save(&mut deps.storage, &pool).unwrap(); + + // Drain denom3 from the pool + pool.exit_pool(&[Coin::new(1000000000000u128, "denom3")]) + .unwrap(); + + let res = transmuter.clean_up_drained_corrupted_assets(&mut deps.storage, &mut pool); + assert_eq!(res, Ok(())); + + // Check that the pool remains unchanged except for the drained assets + let expected_pool = TransmuterPool { + pool_assets: vec![ + Asset::new(Uint128::from(1000000000000u128), "denom1", 1u128).unwrap(), + Asset::new(Uint128::zero(), "denom2", 10u128).unwrap(), + Asset::new(Uint128::zero(), "denom3", 100u128).unwrap(), + ], + asset_groups: BTreeMap::from([( + "group1".to_string(), + AssetGroup::new(vec!["denom2".to_string(), "denom3".to_string()]), + )]), + }; + assert_eq!(pool, expected_pool); + + // Check that the limiter for group1 is still registered + let limiters = transmuter.limiters.list_limiters(&deps.storage).unwrap(); + assert_eq!(limiters.len(), 1); + } } diff --git a/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs b/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs index aca5728..d6e50a9 100644 --- a/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs +++ b/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs @@ -57,25 +57,18 @@ impl TransmuterPool { .any(|asset| asset.denom() == denom && asset.is_corrupted()) } - pub fn remove_corrupted_asset(&mut self, denom: &str) -> Result<(), ContractError> { + pub fn remove_asset(&mut self, denom: &str) -> Result<(), ContractError> { let asset = self.get_pool_asset_by_denom(denom)?; - // make sure that removing asset is corrupted - ensure!( - asset.is_corrupted(), - ContractError::InvalidCorruptedAssetDenom { - denom: denom.to_string() - } - ); // make sure that removing asset has 0 amount ensure!( asset.amount().is_zero(), - ContractError::InvalidCorruptedAssetRemoval {} + ContractError::InvalidAssetRemoval {} ); self.pool_assets.retain(|asset| asset.denom() != denom); - // remove corrupted asset from asset groups + // remove asset from asset groups for label in self.asset_groups.clone().keys() { let asset_group = self.asset_groups.get_mut(label).unwrap(); asset_group.remove_denoms(vec![denom.to_string()]); @@ -644,8 +637,8 @@ mod tests { pool.mark_corrupted_asset("asset2").unwrap(); // Attempt to remove asset2 with non-zero amount (should fail) - let err = pool.remove_corrupted_asset("asset2").unwrap_err(); - assert_eq!(err, ContractError::InvalidCorruptedAssetRemoval {}); + let err = pool.remove_asset("asset2").unwrap_err(); + assert_eq!(err, ContractError::InvalidAssetRemoval {}); // Decrease amount of asset2 to zero for asset in pool.pool_assets.iter_mut() { @@ -655,7 +648,7 @@ mod tests { } // Remove corrupted asset2 (should succeed) - pool.remove_corrupted_asset("asset2").unwrap(); + pool.remove_asset("asset2").unwrap(); assert_eq!( pool.asset_groups, @@ -674,17 +667,8 @@ mod tests { ] ); - // Attempt to remove non-corrupted asset (should fail) - let err = pool.remove_corrupted_asset("asset1").unwrap_err(); - assert_eq!( - err, - ContractError::InvalidCorruptedAssetDenom { - denom: "asset1".to_string() - } - ); - // Attempt to remove non-existent asset (should fail) - let err = pool.remove_corrupted_asset("non_existent").unwrap_err(); + let err = pool.remove_asset("non_existent").unwrap_err(); assert_eq!( err, ContractError::InvalidTransmuteDenom { @@ -704,7 +688,7 @@ mod tests { } // Remove corrupted asset1 - pool.remove_corrupted_asset("asset1").unwrap(); + pool.remove_asset("asset1").unwrap(); // Verify asset1 is removed assert_eq!( From 3f5f9c3c4786fc6f480a0b66cd273a4d774e8aa3 Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Fri, 4 Oct 2024 13:18:53 +0700 Subject: [PATCH 10/26] make sure reset_change_limiter_states instances track asset_group too --- contracts/transmuter/src/contract.rs | 319 ++++++++++++++++++++++++++- contracts/transmuter/src/limiter.rs | 12 +- contracts/transmuter/src/swap.rs | 34 ++- 3 files changed, 348 insertions(+), 17 deletions(-) diff --git a/contracts/transmuter/src/contract.rs b/contracts/transmuter/src/contract.rs index 114b918..63f75d2 100644 --- a/contracts/transmuter/src/contract.rs +++ b/contracts/transmuter/src/contract.rs @@ -227,14 +227,20 @@ impl Transmuter<'_> { pool.add_new_assets(assets)?; self.pool.save(deps.storage, &pool)?; + let asset_weights_iter = pool + .asset_weights()? + .unwrap_or_default() + .into_iter() + .map(|(denom, weight)| (Scope::denom(&denom).key(), weight)); + let asset_group_weights_iter = pool + .asset_group_weights()? + .into_iter() + .map(|(label, weight)| (Scope::asset_group(&label).key(), weight)); + self.limiters.reset_change_limiter_states( deps.storage, env.block.time, - pool.asset_weights()? - .unwrap_or_default() - .into_iter() - .map(|(denom, weight)| (Scope::denom(&denom).key(), weight)) // TODO: handle asset group - .collect(), + asset_weights_iter.chain(asset_group_weights_iter), )?; Ok(Response::new().add_attribute("method", "add_new_assets")) @@ -1130,6 +1136,21 @@ mod tests { let join_pool_msg = ContractExecMsg::Transmuter(ExecMsg::JoinPool {}); execute(deps.as_mut(), env.clone(), info.clone(), join_pool_msg).unwrap(); + // Create asset group + let create_asset_group_msg = ContractExecMsg::Transmuter(ExecMsg::CreateAssetGroup { + label: "group1".to_string(), + denoms: vec!["uosmo".to_string(), "uion".to_string()], + }); + + let info = mock_info(admin, &[]); + execute( + deps.as_mut(), + env.clone(), + info.clone(), + create_asset_group_msg, + ) + .unwrap(); + // set limiters let change_limiter_params = LimiterParams::ChangeLimiter { window_config: WindowConfig { @@ -1143,6 +1164,21 @@ mod tests { upper_limit: Decimal::percent(60), }; + // Register limiter for the asset group + let register_group_limiter_msg = ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { + scope: Scope::AssetGroup("group1".to_string()), + label: "group_change_limiter".to_string(), + limiter_params: change_limiter_params.clone(), + }); + + execute( + deps.as_mut(), + env.clone(), + info.clone(), + register_group_limiter_msg, + ) + .unwrap(); + let info = mock_info(admin, &[]); for denom in ["uosmo", "uion"] { let register_limiter_msg = ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { @@ -1201,6 +1237,12 @@ mod tests { ); } + assert_dirty_change_limiters_by_scope!( + &Scope::asset_group("group1"), + Transmuter::default().limiters, + deps.as_ref().storage + ); + // Add new assets // Attempt to add assets with invalid denom @@ -1274,6 +1316,13 @@ mod tests { ); } + assert_reset_change_limiters_by_scope!( + &Scope::asset_group("group1"), + reset_at, + transmuter, + deps.as_ref().storage + ); + env.block.time = env.block.time.plus_nanos(360); // Check if the new assets were added @@ -1335,7 +1384,6 @@ mod tests { instantiate(deps.as_mut(), env.clone(), info.clone(), init_msg).unwrap(); // Manually reply - let res = reply( deps.as_mut(), env.clone(), @@ -1464,6 +1512,36 @@ mod tests { .unwrap(); } + // Create asset group + let create_asset_group_msg = ContractExecMsg::Transmuter(ExecMsg::CreateAssetGroup { + label: "btc_group1".to_string(), + denoms: vec!["nbtc".to_string(), "stbtc".to_string()], + }); + + let info = mock_info(admin, &[]); + execute( + deps.as_mut(), + env.clone(), + info.clone(), + create_asset_group_msg, + ) + .unwrap(); + + // Register change limiter for the asset group + let register_group_limiter_msg = ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { + scope: Scope::AssetGroup("btc_group1".to_string()), + label: "group_change_limiter".to_string(), + limiter_params: change_limiter_params.clone(), + }); + + execute( + deps.as_mut(), + env.clone(), + info.clone(), + register_group_limiter_msg, + ) + .unwrap(); + // exit pool a bit to make sure the limiters are dirty deps.querier .update_balance("someone", vec![Coin::new(1_000, alloyed_denom.clone())]); @@ -1541,6 +1619,20 @@ mod tests { let info = mock_info("someone", &[]); execute(deps.as_mut(), env.clone(), info.clone(), exit_pool_msg).unwrap(); + for denom in ["wbtc", "tbtc", "nbtc", "stbtc"] { + assert_dirty_change_limiters_by_scope!( + &Scope::denom(denom), + Transmuter::default().limiters, + deps.as_ref().storage + ); + } + + assert_dirty_change_limiters_by_scope!( + &Scope::asset_group("btc_group1"), + Transmuter::default().limiters, + deps.as_ref().storage + ); + let env = increase_block_height(&env, 1); deps.querier @@ -1762,6 +1854,13 @@ mod tests { ); } + assert_reset_change_limiters_by_scope!( + &Scope::asset_group("btc_group1"), + env.block.time, + Transmuter::default(), + deps.as_ref().storage + ); + // try unmark nbtc should fail let unmark_corrupted_assets_msg = ContractExecMsg::Transmuter(ExecMsg::UnmarkCorruptedScopes { @@ -1861,6 +1960,214 @@ mod tests { ); } + #[test] + fn test_corrupted_asset_group() { + let mut deps = mock_dependencies(); + + deps.querier.update_balance( + "admin", + vec![ + Coin::new(1_000_000_000_000, "tbtc"), + Coin::new(1_000_000_000_000, "nbtc"), + Coin::new(1_000_000_000_000, "stbtc"), + ], + ); + + let env = mock_env(); + let info = mock_info("admin", &[]); + + // Initialize contract with asset group + let init_msg = InstantiateMsg { + pool_asset_configs: vec![ + AssetConfig::from_denom_str("tbtc"), + AssetConfig::from_denom_str("nbtc"), + AssetConfig::from_denom_str("stbtc"), + ], + alloyed_asset_subdenom: "btc".to_string(), + alloyed_asset_normalization_factor: Uint128::one(), + admin: Some("admin".to_string()), + moderator: "moderator".to_string(), + }; + + instantiate(deps.as_mut(), env.clone(), info.clone(), init_msg).unwrap(); + + // Manually reply + let res = reply( + deps.as_mut(), + env.clone(), + Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some( + MsgCreateDenomResponse { + new_token_denom: "btc".to_string(), + } + .into(), + ), + }), + }, + ) + .unwrap(); + + let alloyed_denom = res + .attributes + .into_iter() + .find(|attr| attr.key == "alloyed_denom") + .unwrap() + .value; + + deps.querier.update_balance( + "user", + vec![Coin::new(3_000_000_000_000, alloyed_denom.clone())], + ); + + // Create asset group + let create_group_msg = ContractExecMsg::Transmuter(ExecMsg::CreateAssetGroup { + label: "group1".to_string(), + denoms: vec!["tbtc".to_string(), "nbtc".to_string()], + }); + execute(deps.as_mut(), env.clone(), info.clone(), create_group_msg).unwrap(); + + // Set change limiter for btc group + let info = mock_info("admin", &[]); + let set_limiter_msg = ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { + scope: Scope::asset_group("group1"), + label: "big_change_limiter".to_string(), + limiter_params: LimiterParams::ChangeLimiter { + window_config: WindowConfig { + window_size: Uint64::from(3600000000000u64), // 1 hour in nanoseconds + division_count: Uint64::from(6u64), + }, + boundary_offset: Decimal::percent(20), + }, + }); + execute(deps.as_mut(), env.clone(), info.clone(), set_limiter_msg).unwrap(); + + // set change limiter for stbtc + let set_limiter_msg = ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { + scope: Scope::denom("stbtc"), + label: "big_change_limiter".to_string(), + limiter_params: LimiterParams::ChangeLimiter { + window_config: WindowConfig { + window_size: Uint64::from(3600000000000u64), // 1 hour in nanoseconds + division_count: Uint64::from(6u64), + }, + boundary_offset: Decimal::percent(20), + }, + }); + execute(deps.as_mut(), env.clone(), info.clone(), set_limiter_msg).unwrap(); + + // Add some liquidity + let add_liquidity_msg = ContractExecMsg::Transmuter(ExecMsg::JoinPool {}); + execute( + deps.as_mut(), + env.clone(), + mock_info( + "user", + &[ + Coin::new(1_000_000_000_000, "tbtc"), + Coin::new(1_000_000_000_000, "nbtc"), + Coin::new(1_000_000_000_000, "stbtc"), + ], + ), + add_liquidity_msg, + ) + .unwrap(); + + // Assert dirty change limiters for the asset group + assert_dirty_change_limiters_by_scope!( + &Scope::asset_group("group1"), + &Transmuter::default().limiters, + &deps.storage + ); + + // Mark asset group as corrupted + let info = mock_info("moderator", &[]); + let mark_corrupted_msg = ContractExecMsg::Transmuter(ExecMsg::MarkCorruptedScopes { + scopes: vec![Scope::asset_group("group1")], + }); + execute(deps.as_mut(), env.clone(), info.clone(), mark_corrupted_msg).unwrap(); + + // Query corrupted scopes + let res = query( + deps.as_ref(), + env.clone(), + ContractQueryMsg::Transmuter(QueryMsg::GetCorruptedScopes {}), + ) + .unwrap(); + + let GetCorrruptedScopesResponse { corrupted_scopes } = from_json(res).unwrap(); + + assert_eq!(corrupted_scopes, vec![Scope::asset_group("group1")]); + + // Exit pool with all corrupted assets + let env = increase_block_height(&env, 1); + let info = mock_info("user", &[]); + let exit_pool_msg = ContractExecMsg::Transmuter(ExecMsg::ExitPool { + tokens_out: vec![ + Coin::new(1_000_000_000_000, "tbtc"), + Coin::new(1_000_000_000_000, "nbtc"), + ], + }); + execute(deps.as_mut(), env.clone(), info.clone(), exit_pool_msg).unwrap(); + + // Assert reset change limiters for the asset group + assert_reset_change_limiters_by_scope!( + &Scope::asset_group("group1"), + env.block.time, + Transmuter::default(), + &deps.storage + ); + + // Query corrupted scopes again to ensure the asset group is no longer corrupted + let res = query( + deps.as_ref(), + env.clone(), + ContractQueryMsg::Transmuter(QueryMsg::GetCorruptedScopes {}), + ) + .unwrap(); + + let GetCorrruptedScopesResponse { corrupted_scopes } = from_json(res).unwrap(); + + assert!( + corrupted_scopes.is_empty(), + "Corrupted scopes should be empty after exiting pool" + ); + + let msg = ContractQueryMsg::Transmuter(QueryMsg::GetTotalPoolLiquidity {}); + let res = query(deps.as_ref(), env.clone(), msg).unwrap(); + let GetTotalPoolLiquidityResponse { + total_pool_liquidity, + } = from_json(res).unwrap(); + + assert_eq!( + total_pool_liquidity, + vec![Coin::new(1_000_000_000_000, "stbtc")] + ); + + // Assert that only one limiter remains for stbtc + let limiters = Transmuter::default() + .limiters + .list_limiters(&deps.storage) + .unwrap() + .into_iter() + .map(|(k, v)| k) + .collect::>(); + assert_eq!( + limiters, + vec![("denom::stbtc".to_string(), "big_change_limiter".to_string())] + ); + + // Assert reset change limiters for the individual assets + assert_reset_change_limiters_by_scope!( + &Scope::denom("stbtc"), + env.block.time, + Transmuter::default(), + &deps.storage + ); + } + fn increase_block_height(env: &Env, height: u64) -> Env { let block_time = 5; // hypothetical block time Env { diff --git a/contracts/transmuter/src/limiter.rs b/contracts/transmuter/src/limiter.rs index 38c6e60..a517ac1 100644 --- a/contracts/transmuter/src/limiter.rs +++ b/contracts/transmuter/src/limiter.rs @@ -589,7 +589,7 @@ impl<'a> Limiters<'a> { &self, storage: &mut dyn Storage, block_time: Timestamp, - weights: Vec<(String, Decimal)>, + weights: impl Iterator, ) -> Result<(), ContractError> { // there is no need to limit, since the number of limiters is expected to be small let limiters = self.list_limiters(storage)?; @@ -620,13 +620,15 @@ impl<'a> Limiters<'a> { macro_rules! assert_reset_change_limiters_by_scope { ($scope:expr, $reset_at:expr, $transmuter:expr, $storage:expr) => { let pool = $transmuter.pool.load($storage).unwrap(); - let weights = pool + let asset_weights = pool .asset_weights() .unwrap() .unwrap_or_default() .into_iter() .collect::>(); + let asset_group_weights = pool.asset_group_weights().unwrap(); + let limiters = $transmuter .limiters .list_limiters_by_scope($storage, $scope) @@ -635,8 +637,8 @@ macro_rules! assert_reset_change_limiters_by_scope { for (_label, limiter) in limiters { if let $crate::limiter::Limiter::ChangeLimiter(limiter) = limiter { let value = match $scope { - Scope::Denom(denom) => *weights.get(denom.as_str()).unwrap(), - _ => unimplemented!("asset group weight is not supported yet"), + Scope::Denom(denom) => *asset_weights.get(denom.as_str()).unwrap(), + Scope::AssetGroup(label) => *asset_group_weights.get(label.as_str()).unwrap(), }; assert_eq!( limiter.divisions(), @@ -3074,7 +3076,7 @@ mod tests { .reset_change_limiter_states( &mut deps.storage, block_time, - vec![(Scope::denom("denoma").key(), value)], + vec![(Scope::denom("denoma").key(), value)].into_iter(), ) .unwrap(); diff --git a/contracts/transmuter/src/swap.rs b/contracts/transmuter/src/swap.rs index 950741d..d818d3b 100644 --- a/contracts/transmuter/src/swap.rs +++ b/contracts/transmuter/src/swap.rs @@ -280,6 +280,18 @@ impl Transmuter<'_> { }? .to_string(); + let denoms_in_corrupted_asset_group = pool + .asset_groups + .iter() + .flat_map(|(_, asset_group)| { + if asset_group.is_corrupted() { + asset_group.denoms().to_vec() + } else { + vec![] + } + }) + .collect::>(); + let is_force_exit_corrupted_assets = tokens_out.iter().all(|coin| { let total_liquidity = pool .get_pool_asset_by_denom(&coin.denom) @@ -287,8 +299,11 @@ impl Transmuter<'_> { .unwrap_or_default(); let is_redeeming_total_liquidity = coin.amount == total_liquidity; + let is_under_corrupted_asset_group = + denoms_in_corrupted_asset_group.contains(&coin.denom); - pool.is_corrupted_asset(&coin.denom) && is_redeeming_total_liquidity + is_redeeming_total_liquidity + && (is_under_corrupted_asset_group || pool.is_corrupted_asset(&coin.denom)) }); // If all tokens out are corrupted assets and exit with all remaining liquidity @@ -298,14 +313,21 @@ impl Transmuter<'_> { // change limiter needs reset if force redemption since it gets by passed // the current state will not be accurate + + let asset_weights_iter = pool + .asset_weights()? + .unwrap_or_default() + .into_iter() + .map(|(denom, weight)| (Scope::denom(&denom).key(), weight)); + let asset_group_weights_iter = pool + .asset_group_weights()? + .into_iter() + .map(|(label, weight)| (Scope::asset_group(&label).key(), weight)); + self.limiters.reset_change_limiter_states( deps.storage, env.block.time, - pool.asset_weights()? - .unwrap_or_default() - .into_iter() - .map(|(denom, weight)| (Scope::denom(&denom).key(), weight)) // TODO: handle asset group - .collect::>(), + asset_weights_iter.chain(asset_group_weights_iter), )?; } else { let prev_weights = pool.weights_map()?; From d0cd908a97df959e82fdc1f0282243d58830730c Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Fri, 4 Oct 2024 13:38:21 +0700 Subject: [PATCH 11/26] remove unused todo --- contracts/transmuter/src/contract.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/transmuter/src/contract.rs b/contracts/transmuter/src/contract.rs index 63f75d2..62d9cd7 100644 --- a/contracts/transmuter/src/contract.rs +++ b/contracts/transmuter/src/contract.rs @@ -1773,7 +1773,7 @@ mod tests { deps.querier.update_balance( "someone", - vec![Coin::new(1_000_000_000_000, alloyed_denom.clone())], // TODO: increase shares + vec![Coin::new(1_000_000_000_000, alloyed_denom.clone())], ); let all_nbtc = total_liquidity_of("nbtc", &deps.storage); let force_redeem_corrupted_assets_msg = ContractExecMsg::Transmuter(ExecMsg::ExitPool { From 83cc989d6f70eb293ec3a09c39171b50bb0a5a08 Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Fri, 4 Oct 2024 13:38:41 +0700 Subject: [PATCH 12/26] remove unused todo --- simulation/rebalance_incentive.py | 1 - 1 file changed, 1 deletion(-) diff --git a/simulation/rebalance_incentive.py b/simulation/rebalance_incentive.py index c2e0490..c6a9d70 100644 --- a/simulation/rebalance_incentive.py +++ b/simulation/rebalance_incentive.py @@ -164,7 +164,6 @@ def project_point(m): Lower $𝑘$ values provide a more gradual and linear impact, leading to a more moderate fee structure. ''' -# TODO: observation, the more d (= starting point further from midpoint) with the same change in d, the less fee is taken? # we want the opposite, the further away from midpoint, it scales up the fee. # Potentially, Fee / (1-d)?????? From 1cbb3ef4bc5295454dff2d1c1426b9a53397427e Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Mon, 7 Oct 2024 10:38:43 +0700 Subject: [PATCH 13/26] turn assets weights vec into map --- contracts/transmuter/src/contract.rs | 2 +- contracts/transmuter/src/swap.rs | 12 ++++----- .../src/transmuter_pool/asset_group.rs | 7 +---- .../src/transmuter_pool/corrupted_assets.rs | 26 ++++++------------- .../transmuter/src/transmuter_pool/weight.rs | 12 ++------- 5 files changed, 18 insertions(+), 41 deletions(-) diff --git a/contracts/transmuter/src/contract.rs b/contracts/transmuter/src/contract.rs index 62d9cd7..6b0f8a6 100644 --- a/contracts/transmuter/src/contract.rs +++ b/contracts/transmuter/src/contract.rs @@ -2152,7 +2152,7 @@ mod tests { .list_limiters(&deps.storage) .unwrap() .into_iter() - .map(|(k, v)| k) + .map(|(k, _)| k) .collect::>(); assert_eq!( limiters, diff --git a/contracts/transmuter/src/swap.rs b/contracts/transmuter/src/swap.rs index d818d3b..3b1663d 100644 --- a/contracts/transmuter/src/swap.rs +++ b/contracts/transmuter/src/swap.rs @@ -126,7 +126,7 @@ impl Transmuter<'_> { ContractError::ZeroValueOperation {} ); - let prev_weights = pool.weights_map()?; + let prev_weights = pool.asset_weights()?.unwrap_or_default(); pool.join_pool(&tokens_in)?; @@ -330,7 +330,7 @@ impl Transmuter<'_> { asset_weights_iter.chain(asset_group_weights_iter), )?; } else { - let prev_weights = pool.weights_map()?; + let prev_weights = pool.asset_weights()?.unwrap_or_default(); pool.exit_pool(&tokens_out)?; @@ -385,7 +385,7 @@ impl Transmuter<'_> { env: Env, ) -> Result { let pool = self.pool.load(deps.storage)?; - let prev_weights = pool.weights_map()?; + let prev_weights = pool.asset_weights()?.unwrap_or_default(); let (mut pool, actual_token_out) = self.out_amt_given_in(deps.as_ref(), pool, token_in, token_out_denom)?; @@ -443,7 +443,7 @@ impl Transmuter<'_> { env: Env, ) -> Result { let pool = self.pool.load(deps.storage)?; - let prev_weights = pool.weights_map()?; + let prev_weights = pool.asset_weights()?.unwrap_or_default(); let (mut pool, actual_token_in) = self.in_amt_given_out( deps.as_ref(), @@ -672,7 +672,7 @@ impl Transmuter<'_> { fn construct_scope_value_pairs( prev_weights: BTreeMap, - updated_weights: Vec<(String, Decimal)>, + updated_weights: BTreeMap, asset_group: BTreeMap, ) -> Result, StdError> { let mut denom_weight_pairs: HashMap = HashMap::new(); @@ -1775,7 +1775,7 @@ mod tests { .clone() .into_iter() .map(|(denom, (_, updated_weight))| (denom.to_string(), updated_weight)) - .collect_vec(); + .collect(); let mut scope_value_pairs = construct_scope_value_pairs(prev_weights, updated_weights, asset_groups).unwrap(); diff --git a/contracts/transmuter/src/transmuter_pool/asset_group.rs b/contracts/transmuter/src/transmuter_pool/asset_group.rs index 17c546d..88e3f8a 100644 --- a/contracts/transmuter/src/transmuter_pool/asset_group.rs +++ b/contracts/transmuter/src/transmuter_pool/asset_group.rs @@ -128,12 +128,7 @@ impl TransmuterPool { } pub fn asset_group_weights(&self) -> Result, ContractError> { - let denom_weights: BTreeMap<_, _> = self - .asset_weights()? - .unwrap_or_default() - .into_iter() - .collect(); - + let denom_weights = self.asset_weights()?.unwrap_or_default(); let mut weights = BTreeMap::new(); for (label, asset_group) in &self.asset_groups { let mut group_weight = Decimal::zero(); diff --git a/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs b/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs index d6e50a9..20c4a18 100644 --- a/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs +++ b/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs @@ -101,7 +101,6 @@ impl TransmuterPool { // if total pool value == 0 -> Empty mapping, later unwrap weight will be 0 let weight_pre_action = self.asset_weights()?.unwrap_or_default(); - let weight_pre_action = weight_pre_action.into_iter().collect::>(); let res = action(self)?; @@ -113,7 +112,6 @@ impl TransmuterPool { // if total pool value == 0 -> Empty mapping, later unwrap weight will be 0 let weight_post_action = self.asset_weights()?.unwrap_or_default(); - let weight_post_action = weight_post_action.into_iter().collect::>(); for post_action in corrupted_assets_post_action { let denom = post_action.denom().to_string(); @@ -157,14 +155,14 @@ impl TransmuterPool { return action(self); } - let weight_pre_action = self.get_weights()?; + let weight_pre_action = self.asset_weights()?.unwrap_or_default(); let corrupted_asset_groups_state_pre_action = self.get_corrupted_asset_groups_state(&corrupted_asset_groups, &weight_pre_action)?; let res = action(self)?; let corrupted_assets_post_action = self.get_corrupted_assets(); - let weight_post_action = self.get_weights()?; + let weight_post_action = self.asset_weights()?.unwrap_or_default(); let corrupted_asset_groups_state_post_action = self.get_corrupted_asset_groups_state(&corrupted_asset_groups, &weight_post_action)?; @@ -183,7 +181,7 @@ impl TransmuterPool { Ok(res) } - fn get_corrupted_assets(&self) -> HashMap { + fn get_corrupted_assets(&self) -> BTreeMap { self.pool_assets .iter() .filter(|asset| asset.is_corrupted()) @@ -201,20 +199,12 @@ impl TransmuterPool { .collect() } - fn get_weights(&self) -> Result, ContractError> { - Ok(self - .asset_weights()? - .unwrap_or_default() - .into_iter() - .collect()) - } - /// Get the state of corrupted asset groups. /// returns map for label -> (amount, weight) for each asset group fn get_corrupted_asset_groups_state( &self, corrupted_asset_groups: &BTreeMap, - weights: &HashMap, + weights: &BTreeMap, ) -> Result, ContractError> { corrupted_asset_groups .iter() @@ -236,10 +226,10 @@ impl TransmuterPool { fn check_corrupted_assets( &self, - pre_action: &HashMap, - post_action: &HashMap, - weight_pre_action: &HashMap, - weight_post_action: &HashMap, + pre_action: &BTreeMap, + post_action: &BTreeMap, + weight_pre_action: &BTreeMap, + weight_post_action: &BTreeMap, ) -> Result<(), ContractError> { let zero_dec = Decimal::zero(); for (denom, post_asset) in post_action { diff --git a/contracts/transmuter/src/transmuter_pool/weight.rs b/contracts/transmuter/src/transmuter_pool/weight.rs index 4992cdf..b65b514 100644 --- a/contracts/transmuter/src/transmuter_pool/weight.rs +++ b/contracts/transmuter/src/transmuter_pool/weight.rs @@ -20,7 +20,7 @@ impl TransmuterPool { /// /// If total pool asset amount is zero, returns None to signify that /// it makes no sense to calculate ratios, but not an error. - pub fn asset_weights(&self) -> Result>, ContractError> { + pub fn asset_weights(&self) -> Result>, ContractError> { let std_norm_factor = lcm_from_iter( self.pool_assets .iter() @@ -51,14 +51,6 @@ impl TransmuterPool { Ok(Some(ratios)) } - pub fn weights_map(&self) -> Result, ContractError> { - Ok(self - .asset_weights()? - .unwrap_or_default() - .into_iter() - .collect()) - } - fn normalized_asset_values( &self, std_norm_factor: Uint128, @@ -181,7 +173,7 @@ mod tests { }; let ratios = pool.asset_weights().unwrap(); - assert_eq!(ratios, Some(expected)); + assert_eq!(ratios, Some(expected.into_iter().collect())); } #[test] From 8e25224f11d67289d5a28a6ba434945bfd7fd42e Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Mon, 7 Oct 2024 12:07:14 +0700 Subject: [PATCH 14/26] impose max created asset group --- contracts/transmuter/src/error.rs | 3 ++ .../src/transmuter_pool/asset_group.rs | 46 +++++++++++++++++-- .../transmuter/src/transmuter_pool/mod.rs | 4 ++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/contracts/transmuter/src/error.rs b/contracts/transmuter/src/error.rs index c01beb1..56c3325 100644 --- a/contracts/transmuter/src/error.rs +++ b/contracts/transmuter/src/error.rs @@ -57,6 +57,9 @@ pub enum ContractError { actual: Uint64, }, + #[error("Asset group count must be within {max} inclusive, but got: {actual}")] + AssetGroupCountOutOfRange { max: Uint64, actual: Uint64 }, + #[error("Insufficient pool asset: required: {required}, available: {available}")] InsufficientPoolAsset { required: Coin, available: Coin }, diff --git a/contracts/transmuter/src/transmuter_pool/asset_group.rs b/contracts/transmuter/src/transmuter_pool/asset_group.rs index 88e3f8a..a2959bb 100644 --- a/contracts/transmuter/src/transmuter_pool/asset_group.rs +++ b/contracts/transmuter/src/transmuter_pool/asset_group.rs @@ -1,9 +1,9 @@ use std::collections::BTreeMap; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{ensure, Decimal}; +use cosmwasm_std::{ensure, Decimal, Uint64}; -use crate::{corruptable::Corruptable, ContractError}; +use crate::{corruptable::Corruptable, transmuter_pool::MAX_ASSET_GROUPS, ContractError}; use super::TransmuterPool; @@ -109,10 +109,16 @@ impl TransmuterPool { ); } - // TODO: limit sizes of asset groups - self.asset_groups.insert(label, AssetGroup::new(denoms)); + ensure!( + Uint64::from(self.asset_groups.len() as u64) <= MAX_ASSET_GROUPS, + ContractError::AssetGroupCountOutOfRange { + max: MAX_ASSET_GROUPS, + actual: Uint64::new(self.asset_groups.len() as u64) + } + ); + Ok(self) } @@ -236,4 +242,36 @@ mod tests { &Decimal::raw(333333333333333333) ); } + + #[test] + fn test_create_asset_group_within_range() { + let mut pool = TransmuterPool::new(vec![ + Asset::new(Uint128::new(100), "denom1", Uint128::new(1)).unwrap(), + Asset::new(Uint128::new(200), "denom2", Uint128::new(1)).unwrap(), + Asset::new(Uint128::new(300), "denom3", Uint128::new(1)).unwrap(), + ]) + .unwrap(); + + // Test creating groups up to the maximum allowed + for i in 1..=MAX_ASSET_GROUPS.u64() { + let group_name = format!("group{}", i); + let result = pool.create_asset_group(group_name.clone(), vec!["denom1".to_string()]); + assert!(result.is_ok(), "Failed to create group {}", i); + } + + // Attempt to create one more group, which should fail + let result = pool.create_asset_group("extra_group".to_string(), vec!["denom1".to_string()]); + assert!( + result.is_err(), + "Should not be able to create group beyond the maximum" + ); + assert!( + matches!( + result.unwrap_err(), + ContractError::AssetGroupCountOutOfRange { max, actual } + if max == MAX_ASSET_GROUPS && actual == MAX_ASSET_GROUPS + Uint64::one() + ), + "Unexpected error when exceeding max asset groups" + ); + } } diff --git a/contracts/transmuter/src/transmuter_pool/mod.rs b/contracts/transmuter/src/transmuter_pool/mod.rs index b503db8..7a2d110 100644 --- a/contracts/transmuter/src/transmuter_pool/mod.rs +++ b/contracts/transmuter/src/transmuter_pool/mod.rs @@ -25,6 +25,10 @@ const MIN_POOL_ASSET_DENOMS: Uint64 = Uint64::new(1u64); /// prevent the contract from running out of gas when iterating const MAX_POOL_ASSET_DENOMS: Uint64 = Uint64::new(20u64); +/// Maximum number of asset groups allowed in a pool. +/// This limit helps prevent excessive gas consumption when iterating over groups. +const MAX_ASSET_GROUPS: Uint64 = Uint64::new(10u64); + #[cw_serde] pub struct TransmuterPool { pub pool_assets: Vec, From 6934dfb819a552da65522c165e56a96d06c05657 Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Mon, 7 Oct 2024 12:39:20 +0700 Subject: [PATCH 15/26] Remove asset_group const --- contracts/transmuter/src/contract.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/transmuter/src/contract.rs b/contracts/transmuter/src/contract.rs index 6b0f8a6..2e7cd0e 100644 --- a/contracts/transmuter/src/contract.rs +++ b/contracts/transmuter/src/contract.rs @@ -54,7 +54,6 @@ pub mod key { pub const ADMIN: &str = "admin"; pub const MODERATOR: &str = "moderator"; pub const LIMITERS: &str = "limiters"; - pub const ASSET_GROUP: &str = "asset_group"; } #[contract] From 0aecd61ca9d7bbdc1bdeb567a078b1d51c50db5e Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Mon, 7 Oct 2024 13:17:52 +0700 Subject: [PATCH 16/26] implement migration and update old migration tests --- Cargo.lock | 2 +- contracts/transmuter/Cargo.toml | 26 +- contracts/transmuter/src/lib.rs | 4 +- contracts/transmuter/src/migrations/mod.rs | 2 +- contracts/transmuter/src/migrations/v3_2_0.rs | 106 ---- contracts/transmuter/src/migrations/v4_0_0.rs | 161 +++++ contracts/transmuter/src/swap.rs | 1 - .../src/test/cases/units/migrate.rs | 571 +++++++++--------- .../transmuter/testdata/transmuter_v3_2.wasm | Bin 0 -> 704035 bytes 9 files changed, 465 insertions(+), 408 deletions(-) delete mode 100644 contracts/transmuter/src/migrations/v3_2_0.rs create mode 100644 contracts/transmuter/src/migrations/v4_0_0.rs create mode 100644 contracts/transmuter/testdata/transmuter_v3_2.wasm diff --git a/Cargo.lock b/Cargo.lock index 82c7bf3..56dbf61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2344,7 +2344,7 @@ dependencies = [ [[package]] name = "transmuter" -version = "3.2.0" +version = "4.0.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", diff --git a/contracts/transmuter/Cargo.toml b/contracts/transmuter/Cargo.toml index 8ba3e47..51693b2 100644 --- a/contracts/transmuter/Cargo.toml +++ b/contracts/transmuter/Cargo.toml @@ -2,12 +2,12 @@ authors = ["Supanat Potiwarakorn "] edition = "2021" name = "transmuter" -version = "3.2.0" +version = "4.0.0" exclude = [ - # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. - "contract.wasm", - "hash.txt", + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", ] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -42,17 +42,17 @@ optimize = """docker run --rm -v "$(pwd)":/code \ """ [dependencies] -cosmwasm-schema = { workspace = true } -cosmwasm-std = { workspace = true, features = ["cosmwasm_1_1"] } +cosmwasm-schema = {workspace = true} +cosmwasm-std = {workspace = true, features = ["cosmwasm_1_1"]} cosmwasm-storage = "1.3.1" cw-storage-plus = "1.1.0" cw2 = "1.1.0" osmosis-std = "0.22.0" schemars = "0.8.12" -serde = { version = "1.0.183", default-features = false, features = ["derive"] } +serde = {version = "1.0.183", default-features = false, features = ["derive"]} sylvia = "0.10.1" -thiserror = { version = "1.0.44" } -transmuter_math = { version = "1.0.0", path = "../../packages/transmuter_math" } +thiserror = {version = "1.0.44"} +transmuter_math = {version = "1.0.0", path = "../../packages/transmuter_math"} [dev-dependencies] itertools = "0.12.0" @@ -60,7 +60,7 @@ osmosis-test-tube = "22.1.0" rstest = "0.18.2" [lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = [ - 'cfg(tarpaulin)', - 'cfg(tarpaulin_include)', -] } +unexpected_cfgs = {level = "warn", check-cfg = [ + 'cfg(tarpaulin)', + 'cfg(tarpaulin_include)', +]} diff --git a/contracts/transmuter/src/lib.rs b/contracts/transmuter/src/lib.rs index 06760db..af874d1 100644 --- a/contracts/transmuter/src/lib.rs +++ b/contracts/transmuter/src/lib.rs @@ -102,9 +102,9 @@ mod entry_points { pub fn migrate( deps: DepsMut, _env: Env, - _msg: migrations::v3_2_0::MigrateMsg, + _msg: migrations::v4_0_0::MigrateMsg, ) -> Result { - migrations::v3_2_0::execute_migration(deps) + migrations::v4_0_0::execute_migration(deps) } } diff --git a/contracts/transmuter/src/migrations/mod.rs b/contracts/transmuter/src/migrations/mod.rs index 8d355b1..b0da2e1 100644 --- a/contracts/transmuter/src/migrations/mod.rs +++ b/contracts/transmuter/src/migrations/mod.rs @@ -1 +1 @@ -pub mod v3_2_0; +pub mod v4_0_0; diff --git a/contracts/transmuter/src/migrations/v3_2_0.rs b/contracts/transmuter/src/migrations/v3_2_0.rs deleted file mode 100644 index 3c2340f..0000000 --- a/contracts/transmuter/src/migrations/v3_2_0.rs +++ /dev/null @@ -1,106 +0,0 @@ -use cosmwasm_schema::cw_serde; - -use cosmwasm_std::{ensure_eq, DepsMut, Response, Storage}; -use cw2::{ContractVersion, VersionError, CONTRACT}; - -use crate::{ - contract::{CONTRACT_NAME, CONTRACT_VERSION}, - ContractError, -}; - -const FROM_VERSIONS: &[&str] = &["3.0.0", "3.1.0"]; -const TO_VERSION: &str = "3.2.0"; - -#[cw_serde] -pub struct MigrateMsg {} - -pub fn execute_migration(deps: DepsMut) -> Result { - // Assert that the stored contract version matches the expected version before migration - assert_contract_versions(deps.storage, CONTRACT_NAME, FROM_VERSIONS)?; - - // Ensure that the current contract version matches the target version to prevent migration to an incorrect version - ensure_eq!( - CONTRACT_VERSION, - TO_VERSION, - cw2::VersionError::WrongVersion { - expected: TO_VERSION.to_string(), - found: CONTRACT_VERSION.to_string() - } - ); - - // Set the contract version to the target version after successful migration - cw2::set_contract_version(deps.storage, CONTRACT_NAME, TO_VERSION)?; - - // Return a response with an attribute indicating the method that was executed - Ok(Response::new().add_attribute("method", "v3_2_0/execute_migraiton")) -} - -/// Assert that the stored contract version info matches the given value. -/// This is useful during migrations, for making sure that the correct contract -/// is being migrated, and it's being migrated from the correct version. -fn assert_contract_versions( - storage: &dyn Storage, - expected_contract: &str, - expected_versions: &[&str], -) -> Result<(), VersionError> { - let ContractVersion { contract, version } = match CONTRACT.may_load(storage)? { - Some(contract) => contract, - None => return Err(VersionError::NotFound), - }; - - if contract != expected_contract { - return Err(VersionError::WrongContract { - expected: expected_contract.into(), - found: contract, - }); - } - - if !expected_versions.contains(&version.as_str()) { - return Err(VersionError::WrongVersion { - expected: expected_versions.join(","), - found: version, - }); - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use cosmwasm_std::testing::mock_dependencies; - - use super::*; - - #[test] - fn test_successful_migration() { - let mut deps = mock_dependencies(); - - for from_version in FROM_VERSIONS { - cw2::set_contract_version(&mut deps.storage, CONTRACT_NAME, from_version.to_string()) - .unwrap(); - - let res = execute_migration(deps.as_mut()).unwrap(); - - assert_eq!( - res, - Response::new().add_attribute("method", "v3_2_0/execute_migraiton") - ); - } - } - - #[test] - fn test_invalid_version() { - let mut deps = mock_dependencies(); - - cw2::set_contract_version(&mut deps.storage, CONTRACT_NAME, "2.0.0").unwrap(); - - let err = execute_migration(deps.as_mut()).unwrap_err(); - assert_eq!( - err, - ContractError::VersionError(cw2::VersionError::WrongVersion { - expected: FROM_VERSIONS.join(","), - found: "2.0.0".to_string() - }) - ); - } -} diff --git a/contracts/transmuter/src/migrations/v4_0_0.rs b/contracts/transmuter/src/migrations/v4_0_0.rs new file mode 100644 index 0000000..0ba6a42 --- /dev/null +++ b/contracts/transmuter/src/migrations/v4_0_0.rs @@ -0,0 +1,161 @@ +use std::collections::BTreeMap; + +use cosmwasm_schema::cw_serde; + +use cosmwasm_std::{ensure_eq, DepsMut, Response, Storage}; +use cw2::{ContractVersion, VersionError, CONTRACT}; +use cw_storage_plus::Item; + +use crate::{ + asset::Asset, + contract::{key, CONTRACT_NAME, CONTRACT_VERSION}, + transmuter_pool::TransmuterPool, + ContractError, +}; + +const FROM_VERSION: &str = "3.2.0"; +const TO_VERSION: &str = "4.0.0"; + +#[cw_serde] +pub struct MigrateMsg {} + +#[cw_serde] +pub struct TransmuterPoolV3 { + pub pool_assets: Vec, + // [to-be-added] pub asset_groups: BTreeMap, +} + +pub fn execute_migration(deps: DepsMut) -> Result { + // Assert that the stored contract version matches the expected version before migration + assert_contract_versions(deps.storage, CONTRACT_NAME, FROM_VERSION)?; + + // Ensure that the current contract version matches the target version to prevent migration to an incorrect version + ensure_eq!( + CONTRACT_VERSION, + TO_VERSION, + cw2::VersionError::WrongVersion { + expected: TO_VERSION.to_string(), + found: CONTRACT_VERSION.to_string() + } + ); + + // add asset groups to the pool + let pool_v3: TransmuterPoolV3 = + Item::<'_, TransmuterPoolV3>::new(key::POOL).load(deps.storage)?; + + let pool_v4 = TransmuterPool { + pool_assets: pool_v3.pool_assets, + asset_groups: BTreeMap::new(), + }; + + Item::<'_, TransmuterPool>::new(key::POOL).save(deps.storage, &pool_v4)?; + + // Set the contract version to the target version after successful migration + cw2::set_contract_version(deps.storage, CONTRACT_NAME, TO_VERSION)?; + + // Return a response with an attribute indicating the method that was executed + Ok(Response::new().add_attribute("method", "v4_0_0/execute_migraiton")) +} + +/// Assert that the stored contract version info matches the given value. +/// This is useful during migrations, for making sure that the correct contract +/// is being migrated, and it's being migrated from the correct version. +fn assert_contract_versions( + storage: &dyn Storage, + expected_contract: &str, + expected_version: &str, +) -> Result<(), VersionError> { + let ContractVersion { contract, version } = match CONTRACT.may_load(storage)? { + Some(contract) => contract, + None => return Err(VersionError::NotFound), + }; + + if contract != expected_contract { + return Err(VersionError::WrongContract { + expected: expected_contract.into(), + found: contract, + }); + } + + if version.as_str() != expected_version { + return Err(VersionError::WrongVersion { + expected: expected_version.to_string(), + found: version, + }); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use cosmwasm_std::{testing::mock_dependencies, Uint128}; + + use super::*; + + #[test] + fn test_successful_migration() { + let mut deps = mock_dependencies(); + + cw2::set_contract_version(&mut deps.storage, CONTRACT_NAME, FROM_VERSION).unwrap(); + + let pool_assets = vec![ + Asset::new(Uint128::from(100u128), "uusdt", Uint128::from(1u128)).unwrap(), + Asset::new(Uint128::from(200u128), "uusdc", Uint128::from(1u128)).unwrap(), + ]; + let pool_v3 = TransmuterPoolV3 { + pool_assets: pool_assets.clone(), + }; + + Item::new(key::POOL) + .save(&mut deps.storage, &pool_v3) + .unwrap(); + + let res = execute_migration(deps.as_mut()).unwrap(); + + let pool = Item::<'_, TransmuterPool>::new(key::POOL) + .load(&deps.storage) + .unwrap(); + + assert_eq!( + pool, + TransmuterPool { + pool_assets, + asset_groups: BTreeMap::new() // migrgate with empty asset groups + } + ); + + assert_eq!( + res, + Response::new().add_attribute("method", "v4_0_0/execute_migraiton") + ); + } + + #[test] + fn test_invalid_version() { + let mut deps = mock_dependencies(); + + let pool_assets = vec![ + Asset::new(Uint128::from(100u128), "uusdt", Uint128::from(1u128)).unwrap(), + Asset::new(Uint128::from(200u128), "uusdc", Uint128::from(1u128)).unwrap(), + ]; + let pool_v3 = TransmuterPoolV3 { + pool_assets: pool_assets.clone(), + }; + + Item::new(key::POOL) + .save(&mut deps.storage, &pool_v3) + .unwrap(); + + cw2::set_contract_version(&mut deps.storage, CONTRACT_NAME, "3.0.0").unwrap(); + + let err = execute_migration(deps.as_mut()).unwrap_err(); + assert_eq!( + err, + ContractError::VersionError(cw2::VersionError::WrongVersion { + expected: FROM_VERSION.to_string(), + found: "3.0.0".to_string() + }) + ); + } +} diff --git a/contracts/transmuter/src/swap.rs b/contracts/transmuter/src/swap.rs index 3b1663d..081f221 100644 --- a/contracts/transmuter/src/swap.rs +++ b/contracts/transmuter/src/swap.rs @@ -679,7 +679,6 @@ fn construct_scope_value_pairs( let mut asset_group_weight_pairs: HashMap = HashMap::new(); // Reverse index the asset groups - // TODO: handle cases where asset group contains denom that does not exist let mut asset_groups_of_denom = HashMap::new(); for (group, asset_group) in asset_group { for denom in asset_group.into_denoms() { diff --git a/contracts/transmuter/src/test/cases/units/migrate.rs b/contracts/transmuter/src/test/cases/units/migrate.rs index 81d5ec3..4614284 100644 --- a/contracts/transmuter/src/test/cases/units/migrate.rs +++ b/contracts/transmuter/src/test/cases/units/migrate.rs @@ -1,284 +1,287 @@ -// use std::{iter, path::PathBuf}; - -// use crate::{ -// asset::AssetConfig, -// contract::{ -// sv::{InstantiateMsg, QueryMsg}, -// GetModeratorResponse, ListAssetConfigsResponse, -// }, -// migrations::v3_2_0::MigrateMsg, -// test::{modules::cosmwasm_pool::CosmwasmPool, test_env::TransmuterContract}, -// }; -// use cosmwasm_schema::cw_serde; -// use cosmwasm_std::{from_json, to_json_binary, Coin, Uint128}; -// use osmosis_std::types::{ -// cosmwasm::wasm::v1::{QueryRawContractStateRequest, QueryRawContractStateResponse}, -// osmosis::cosmwasmpool::v1beta1::{ -// ContractInfoByPoolIdRequest, ContractInfoByPoolIdResponse, MigratePoolContractsProposal, -// MsgCreateCosmWasmPool, UploadCosmWasmPoolCodeAndWhiteListProposal, -// }, -// }; -// use osmosis_test_tube::{Account, GovWithAppAccess, Module, OsmosisTestApp, Runner}; -// use rstest::rstest; - -// #[cw_serde] -// struct InstantiateMsgV2 { -// pool_asset_denoms: Vec, -// alloyed_asset_subdenom: String, -// admin: Option, -// moderator: Option, -// } - -// #[cw_serde] -// struct MigrateMsgV3 { -// asset_configs: Vec, -// alloyed_asset_normalization_factor: Uint128, -// moderator: Option, -// } - -// #[test] -// fn test_migrate_v2_to_v3() { -// // --- setup account --- -// let app = OsmosisTestApp::new(); -// let signer = app -// .init_account(&[ -// Coin::new(100000, "denom1"), -// Coin::new(100000, "denom2"), -// Coin::new(10000000000000, "uosmo"), -// ]) -// .unwrap(); - -// // --- create pool ---- - -// let cp = CosmwasmPool::new(&app); -// let gov = GovWithAppAccess::new(&app); -// gov.propose_and_execute( -// UploadCosmWasmPoolCodeAndWhiteListProposal::TYPE_URL.to_string(), -// UploadCosmWasmPoolCodeAndWhiteListProposal { -// title: String::from("store test cosmwasm pool code"), -// description: String::from("test"), -// wasm_byte_code: get_prev_version_of_wasm_byte_code("v2"), -// }, -// signer.address(), -// &signer, -// ) -// .unwrap(); - -// let instantiate_msg = InstantiateMsgV2 { -// pool_asset_denoms: vec!["denom1".to_string(), "denom2".to_string()], -// alloyed_asset_subdenom: "denomx".to_string(), -// admin: Some(signer.address()), -// moderator: None, -// }; - -// let code_id = 1; -// let res = cp -// .create_cosmwasm_pool( -// MsgCreateCosmWasmPool { -// code_id, -// instantiate_msg: to_json_binary(&instantiate_msg).unwrap().to_vec(), -// sender: signer.address(), -// }, -// &signer, -// ) -// .unwrap(); - -// let pool_id = res.data.pool_id; - -// let ContractInfoByPoolIdResponse { -// contract_address, -// code_id: _, -// } = cp -// .contract_info_by_pool_id(&ContractInfoByPoolIdRequest { pool_id }) -// .unwrap(); - -// let t = TransmuterContract::new(&app, code_id, pool_id, contract_address.clone()); - -// // --- migrate pool --- -// let migrate_msg = MigrateMsgV3 { -// asset_configs: vec![ -// AssetConfig { -// denom: "denom1".to_string(), -// normalization_factor: Uint128::new(1), -// }, -// AssetConfig { -// denom: "denom2".to_string(), -// normalization_factor: Uint128::new(10000), -// }, -// ], -// alloyed_asset_normalization_factor: Uint128::new(10), -// moderator: Some("osmo1cyyzpxplxdzkeea7kwsydadg87357qnahakaks".to_string()), -// }; - -// gov.propose_and_execute( -// MigratePoolContractsProposal::TYPE_URL.to_string(), -// MigratePoolContractsProposal { -// title: "migrate cosmwasmpool".to_string(), -// description: "test migration".to_string(), -// pool_ids: vec![pool_id], -// new_code_id: 0, // upload new code, so set this to 0 -// wasm_byte_code: get_prev_version_of_wasm_byte_code("v3"), -// migrate_msg: to_json_binary(&migrate_msg).unwrap().to_vec(), -// }, -// signer.address(), -// &signer, -// ) -// .unwrap(); - -// let alloyed_denom = format!("factory/{contract_address}/alloyed/denomx"); - -// let expected_asset_configs = migrate_msg -// .asset_configs -// .into_iter() -// .chain(iter::once(AssetConfig { -// denom: alloyed_denom, -// normalization_factor: migrate_msg.alloyed_asset_normalization_factor, -// })) -// .collect::>(); - -// // list asset configs -// let ListAssetConfigsResponse { asset_configs } = -// t.query(&QueryMsg::ListAssetConfigs {}).unwrap(); - -// assert_eq!(asset_configs, expected_asset_configs); - -// let GetModeratorResponse { moderator } = t.query(&QueryMsg::GetModerator {}).unwrap(); -// assert_eq!(moderator, migrate_msg.moderator.unwrap()); -// } - -// #[rstest] -// #[case("v3")] -// #[case("v3_1")] -// fn test_migrate_v3(#[case] from_version: &str) { -// // --- setup account --- -// let app = OsmosisTestApp::new(); -// let signer = app -// .init_account(&[ -// Coin::new(100000, "denom1"), -// Coin::new(100000, "denom2"), -// Coin::new(10000000000000, "uosmo"), -// ]) -// .unwrap(); - -// // --- create pool ---- - -// let cp = CosmwasmPool::new(&app); -// let gov = GovWithAppAccess::new(&app); -// gov.propose_and_execute( -// UploadCosmWasmPoolCodeAndWhiteListProposal::TYPE_URL.to_string(), -// UploadCosmWasmPoolCodeAndWhiteListProposal { -// title: String::from("store test cosmwasm pool code"), -// description: String::from("test"), -// wasm_byte_code: get_prev_version_of_wasm_byte_code(from_version), -// }, -// signer.address(), -// &signer, -// ) -// .unwrap(); - -// let instantiate_msg = InstantiateMsg { -// pool_asset_configs: vec![ -// AssetConfig { -// denom: "denom1".to_string(), -// normalization_factor: Uint128::new(1), -// }, -// AssetConfig { -// denom: "denom2".to_string(), -// normalization_factor: Uint128::new(10000), -// }, -// ], -// alloyed_asset_subdenom: "denomx".to_string(), -// alloyed_asset_normalization_factor: Uint128::new(10), -// admin: Some(signer.address()), -// moderator: signer.address(), -// }; - -// let code_id = 1; -// let res = cp -// .create_cosmwasm_pool( -// MsgCreateCosmWasmPool { -// code_id, -// instantiate_msg: to_json_binary(&instantiate_msg).unwrap().to_vec(), -// sender: signer.address(), -// }, -// &signer, -// ) -// .unwrap(); - -// let pool_id = res.data.pool_id; - -// let ContractInfoByPoolIdResponse { -// contract_address, -// code_id: _, -// } = cp -// .contract_info_by_pool_id(&ContractInfoByPoolIdRequest { pool_id }) -// .unwrap(); - -// let t = TransmuterContract::new(&app, code_id, pool_id, contract_address.clone()); - -// // --- migrate pool --- -// let migrate_msg = MigrateMsg {}; - -// gov.propose_and_execute( -// MigratePoolContractsProposal::TYPE_URL.to_string(), -// MigratePoolContractsProposal { -// title: "migrate cosmwasmpool".to_string(), -// description: "test migration".to_string(), -// pool_ids: vec![pool_id], -// new_code_id: 0, // upload new code, so set this to 0 -// wasm_byte_code: TransmuterContract::get_wasm_byte_code(), -// migrate_msg: to_json_binary(&migrate_msg).unwrap().to_vec(), -// }, -// signer.address(), -// &signer, -// ) -// .unwrap(); - -// let alloyed_denom = format!("factory/{contract_address}/alloyed/denomx"); - -// let expected_asset_configs = instantiate_msg -// .pool_asset_configs -// .into_iter() -// .chain(iter::once(AssetConfig { -// denom: alloyed_denom, -// normalization_factor: instantiate_msg.alloyed_asset_normalization_factor, -// })) -// .collect::>(); - -// // list asset configs -// let ListAssetConfigsResponse { asset_configs } = -// t.query(&QueryMsg::ListAssetConfigs {}).unwrap(); - -// // expect no changes in asset config -// assert_eq!(asset_configs, expected_asset_configs); - -// let res: QueryRawContractStateResponse = app -// .query( -// "/cosmwasm.wasm.v1.Query/RawContractState", -// &QueryRawContractStateRequest { -// address: t.contract_addr, -// query_data: b"contract_info".to_vec(), -// }, -// ) -// .unwrap(); - -// let version: cw2::ContractVersion = from_json(res.data).unwrap(); - -// assert_eq!( -// version, -// cw2::ContractVersion { -// contract: "crates.io:transmuter".to_string(), -// version: "3.2.0".to_string() -// } -// ); -// } - -// fn get_prev_version_of_wasm_byte_code(version: &str) -> Vec { -// let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); -// let wasm_path = manifest_path -// .join("testdata") -// .join(format!("transmuter_{version}.wasm")); - -// let err_msg = &format!("failed to read wasm file: {}", wasm_path.display()); -// std::fs::read(wasm_path).expect(err_msg) -// } +use std::{iter, path::PathBuf}; + +use crate::{ + asset::AssetConfig, + contract::{ + sv::{InstantiateMsg, QueryMsg}, + GetModeratorResponse, ListAssetConfigsResponse, + }, + migrations::v4_0_0::MigrateMsg, + test::{modules::cosmwasm_pool::CosmwasmPool, test_env::TransmuterContract}, +}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{from_json, to_json_binary, Coin, Uint128}; +use osmosis_std::types::{ + cosmwasm::wasm::v1::{QueryRawContractStateRequest, QueryRawContractStateResponse}, + osmosis::cosmwasmpool::v1beta1::{ + ContractInfoByPoolIdRequest, ContractInfoByPoolIdResponse, MigratePoolContractsProposal, + MsgCreateCosmWasmPool, UploadCosmWasmPoolCodeAndWhiteListProposal, + }, +}; +use osmosis_test_tube::{Account, GovWithAppAccess, Module, OsmosisTestApp, Runner}; +use rstest::rstest; + +#[cw_serde] +struct InstantiateMsgV2 { + pool_asset_denoms: Vec, + alloyed_asset_subdenom: String, + admin: Option, + moderator: Option, +} + +#[cw_serde] +struct MigrateMsgV3 { + asset_configs: Vec, + alloyed_asset_normalization_factor: Uint128, + moderator: Option, +} + +#[test] +fn test_migrate_v2_to_v3() { + // --- setup account --- + let app = OsmosisTestApp::new(); + let signer = app + .init_account(&[ + Coin::new(100000, "denom1"), + Coin::new(100000, "denom2"), + Coin::new(10000000000000, "uosmo"), + ]) + .unwrap(); + + // --- create pool ---- + + let cp = CosmwasmPool::new(&app); + let gov = GovWithAppAccess::new(&app); + gov.propose_and_execute( + UploadCosmWasmPoolCodeAndWhiteListProposal::TYPE_URL.to_string(), + UploadCosmWasmPoolCodeAndWhiteListProposal { + title: String::from("store test cosmwasm pool code"), + description: String::from("test"), + wasm_byte_code: get_prev_version_of_wasm_byte_code("v2"), + }, + signer.address(), + &signer, + ) + .unwrap(); + + let instantiate_msg = InstantiateMsgV2 { + pool_asset_denoms: vec!["denom1".to_string(), "denom2".to_string()], + alloyed_asset_subdenom: "denomx".to_string(), + admin: Some(signer.address()), + moderator: None, + }; + + let code_id = 1; + let res = cp + .create_cosmwasm_pool( + MsgCreateCosmWasmPool { + code_id, + instantiate_msg: to_json_binary(&instantiate_msg).unwrap().to_vec(), + sender: signer.address(), + }, + &signer, + ) + .unwrap(); + + let pool_id = res.data.pool_id; + + let ContractInfoByPoolIdResponse { + contract_address, + code_id: _, + } = cp + .contract_info_by_pool_id(&ContractInfoByPoolIdRequest { pool_id }) + .unwrap(); + + let t = TransmuterContract::new(&app, code_id, pool_id, contract_address.clone()); + + // --- migrate pool --- + let migrate_msg = MigrateMsgV3 { + asset_configs: vec![ + AssetConfig { + denom: "denom1".to_string(), + normalization_factor: Uint128::new(1), + }, + AssetConfig { + denom: "denom2".to_string(), + normalization_factor: Uint128::new(10000), + }, + ], + alloyed_asset_normalization_factor: Uint128::new(10), + moderator: Some("osmo1cyyzpxplxdzkeea7kwsydadg87357qnahakaks".to_string()), + }; + + gov.propose_and_execute( + MigratePoolContractsProposal::TYPE_URL.to_string(), + MigratePoolContractsProposal { + title: "migrate cosmwasmpool".to_string(), + description: "test migration".to_string(), + pool_ids: vec![pool_id], + new_code_id: 0, // upload new code, so set this to 0 + wasm_byte_code: get_prev_version_of_wasm_byte_code("v3"), + migrate_msg: to_json_binary(&migrate_msg).unwrap().to_vec(), + }, + signer.address(), + &signer, + ) + .unwrap(); + + let alloyed_denom = format!("factory/{contract_address}/alloyed/denomx"); + + let expected_asset_configs = migrate_msg + .asset_configs + .into_iter() + .chain(iter::once(AssetConfig { + denom: alloyed_denom, + normalization_factor: migrate_msg.alloyed_asset_normalization_factor, + })) + .collect::>(); + + // list asset configs + let ListAssetConfigsResponse { asset_configs } = + t.query(&QueryMsg::ListAssetConfigs {}).unwrap(); + + assert_eq!(asset_configs, expected_asset_configs); + + let GetModeratorResponse { moderator } = t.query(&QueryMsg::GetModerator {}).unwrap(); + assert_eq!(moderator, migrate_msg.moderator.unwrap()); +} + +#[cw_serde] +struct MigrateMsgV3_2 {} + +#[rstest] +#[case("v3")] +#[case("v3_1")] +fn test_migrate_v3_2(#[case] from_version: &str) { + // --- setup account --- + let app = OsmosisTestApp::new(); + let signer = app + .init_account(&[ + Coin::new(100000, "denom1"), + Coin::new(100000, "denom2"), + Coin::new(10000000000000, "uosmo"), + ]) + .unwrap(); + + // --- create pool ---- + + let cp = CosmwasmPool::new(&app); + let gov = GovWithAppAccess::new(&app); + gov.propose_and_execute( + UploadCosmWasmPoolCodeAndWhiteListProposal::TYPE_URL.to_string(), + UploadCosmWasmPoolCodeAndWhiteListProposal { + title: String::from("store test cosmwasm pool code"), + description: String::from("test"), + wasm_byte_code: get_prev_version_of_wasm_byte_code(from_version), + }, + signer.address(), + &signer, + ) + .unwrap(); + + let instantiate_msg = InstantiateMsg { + pool_asset_configs: vec![ + AssetConfig { + denom: "denom1".to_string(), + normalization_factor: Uint128::new(1), + }, + AssetConfig { + denom: "denom2".to_string(), + normalization_factor: Uint128::new(10000), + }, + ], + alloyed_asset_subdenom: "denomx".to_string(), + alloyed_asset_normalization_factor: Uint128::new(10), + admin: Some(signer.address()), + moderator: signer.address(), + }; + + let code_id = 1; + let res = cp + .create_cosmwasm_pool( + MsgCreateCosmWasmPool { + code_id, + instantiate_msg: to_json_binary(&instantiate_msg).unwrap().to_vec(), + sender: signer.address(), + }, + &signer, + ) + .unwrap(); + + let pool_id = res.data.pool_id; + + let ContractInfoByPoolIdResponse { + contract_address, + code_id: _, + } = cp + .contract_info_by_pool_id(&ContractInfoByPoolIdRequest { pool_id }) + .unwrap(); + + let t = TransmuterContract::new(&app, code_id, pool_id, contract_address.clone()); + + // --- migrate pool --- + let migrate_msg = MigrateMsgV3_2 {}; + + gov.propose_and_execute( + MigratePoolContractsProposal::TYPE_URL.to_string(), + MigratePoolContractsProposal { + title: "migrate cosmwasmpool".to_string(), + description: "test migration".to_string(), + pool_ids: vec![pool_id], + new_code_id: 0, // upload new code, so set this to 0 + wasm_byte_code: get_prev_version_of_wasm_byte_code("v3_2"), + migrate_msg: to_json_binary(&migrate_msg).unwrap().to_vec(), + }, + signer.address(), + &signer, + ) + .unwrap(); + + let alloyed_denom = format!("factory/{contract_address}/alloyed/denomx"); + + let expected_asset_configs = instantiate_msg + .pool_asset_configs + .into_iter() + .chain(iter::once(AssetConfig { + denom: alloyed_denom, + normalization_factor: instantiate_msg.alloyed_asset_normalization_factor, + })) + .collect::>(); + + // list asset configs + let ListAssetConfigsResponse { asset_configs } = + t.query(&QueryMsg::ListAssetConfigs {}).unwrap(); + + // expect no changes in asset config + assert_eq!(asset_configs, expected_asset_configs); + + let res: QueryRawContractStateResponse = app + .query( + "/cosmwasm.wasm.v1.Query/RawContractState", + &QueryRawContractStateRequest { + address: t.contract_addr, + query_data: b"contract_info".to_vec(), + }, + ) + .unwrap(); + + let version: cw2::ContractVersion = from_json(res.data).unwrap(); + + assert_eq!( + version, + cw2::ContractVersion { + contract: "crates.io:transmuter".to_string(), + version: "3.2.0".to_string() + } + ); +} + +fn get_prev_version_of_wasm_byte_code(version: &str) -> Vec { + let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let wasm_path = manifest_path + .join("testdata") + .join(format!("transmuter_{version}.wasm")); + + let err_msg = &format!("failed to read wasm file: {}", wasm_path.display()); + std::fs::read(wasm_path).expect(err_msg) +} diff --git a/contracts/transmuter/testdata/transmuter_v3_2.wasm b/contracts/transmuter/testdata/transmuter_v3_2.wasm new file mode 100644 index 0000000000000000000000000000000000000000..90525a025f854b2ba43dbf77c2a5b7ef41bcad86 GIT binary patch literal 704035 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%0Ar}{H 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%0D8l3OaNiSsOLi@Y@( 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

(YD`c+`2!oxy5l)G&5zn{%sOE3AX#fT)ko`r;~S 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 literal 0 HcmV?d00001 From c7847db2b130988ae864cf4a728031868046e2f3 Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Mon, 7 Oct 2024 13:36:50 +0700 Subject: [PATCH 17/26] integration test the migration --- .../src/test/cases/units/migrate.rs | 141 +++++++++++++++++- 1 file changed, 139 insertions(+), 2 deletions(-) diff --git a/contracts/transmuter/src/test/cases/units/migrate.rs b/contracts/transmuter/src/test/cases/units/migrate.rs index 4614284..1b47a5d 100644 --- a/contracts/transmuter/src/test/cases/units/migrate.rs +++ b/contracts/transmuter/src/test/cases/units/migrate.rs @@ -1,10 +1,10 @@ -use std::{iter, path::PathBuf}; +use std::{collections::BTreeMap, iter, path::PathBuf}; use crate::{ asset::AssetConfig, contract::{ sv::{InstantiateMsg, QueryMsg}, - GetModeratorResponse, ListAssetConfigsResponse, + GetModeratorResponse, ListAssetConfigsResponse, ListAssetGroupsResponse, }, migrations::v4_0_0::MigrateMsg, test::{modules::cosmwasm_pool::CosmwasmPool, test_env::TransmuterContract}, @@ -276,6 +276,143 @@ fn test_migrate_v3_2(#[case] from_version: &str) { ); } +#[test] +fn test_migrate_v4_0_0() { + let from_version = "v3_2"; + // --- setup account --- + let app = OsmosisTestApp::new(); + let signer = app + .init_account(&[ + Coin::new(100000, "denom1"), + Coin::new(100000, "denom2"), + Coin::new(10000000000000, "uosmo"), + ]) + .unwrap(); + + // --- create pool ---- + + let cp = CosmwasmPool::new(&app); + let gov = GovWithAppAccess::new(&app); + gov.propose_and_execute( + UploadCosmWasmPoolCodeAndWhiteListProposal::TYPE_URL.to_string(), + UploadCosmWasmPoolCodeAndWhiteListProposal { + title: String::from("store test cosmwasm pool code"), + description: String::from("test"), + wasm_byte_code: get_prev_version_of_wasm_byte_code(from_version), + }, + signer.address(), + &signer, + ) + .unwrap(); + + let instantiate_msg = InstantiateMsg { + pool_asset_configs: vec![ + AssetConfig { + denom: "denom1".to_string(), + normalization_factor: Uint128::new(1), + }, + AssetConfig { + denom: "denom2".to_string(), + normalization_factor: Uint128::new(10000), + }, + ], + alloyed_asset_subdenom: "denomx".to_string(), + alloyed_asset_normalization_factor: Uint128::new(10), + admin: Some(signer.address()), + moderator: signer.address(), + }; + + let code_id = 1; + let res = cp + .create_cosmwasm_pool( + MsgCreateCosmWasmPool { + code_id, + instantiate_msg: to_json_binary(&instantiate_msg).unwrap().to_vec(), + sender: signer.address(), + }, + &signer, + ) + .unwrap(); + + let pool_id = res.data.pool_id; + + let ContractInfoByPoolIdResponse { + contract_address, + code_id: _, + } = cp + .contract_info_by_pool_id(&ContractInfoByPoolIdRequest { pool_id }) + .unwrap(); + + let t = TransmuterContract::new(&app, code_id, pool_id, contract_address.clone()); + + // --- migrate pool --- + let migrate_msg = MigrateMsg {}; + + gov.propose_and_execute( + MigratePoolContractsProposal::TYPE_URL.to_string(), + MigratePoolContractsProposal { + title: "migrate cosmwasmpool".to_string(), + description: "test migration".to_string(), + pool_ids: vec![pool_id], + new_code_id: 0, // upload new code, so set this to 0 + wasm_byte_code: TransmuterContract::get_wasm_byte_code(), + migrate_msg: to_json_binary(&migrate_msg).unwrap().to_vec(), + }, + signer.address(), + &signer, + ) + .unwrap(); + + let alloyed_denom = format!("factory/{contract_address}/alloyed/denomx"); + + let expected_asset_configs = instantiate_msg + .pool_asset_configs + .into_iter() + .chain(iter::once(AssetConfig { + denom: alloyed_denom, + normalization_factor: instantiate_msg.alloyed_asset_normalization_factor, + })) + .collect::>(); + + // list asset configs + let ListAssetConfigsResponse { asset_configs } = + t.query(&QueryMsg::ListAssetConfigs {}).unwrap(); + + // expect no changes in asset config + assert_eq!(asset_configs, expected_asset_configs); + + let res: QueryRawContractStateResponse = app + .query( + "/cosmwasm.wasm.v1.Query/RawContractState", + &QueryRawContractStateRequest { + address: t.contract_addr.clone(), + query_data: b"contract_info".to_vec(), + }, + ) + .unwrap(); + + let version: cw2::ContractVersion = from_json(res.data).unwrap(); + + assert_eq!( + version, + cw2::ContractVersion { + contract: "crates.io:transmuter".to_string(), + version: "4.0.0".to_string() + } + ); + + // asset configs should be the same + let ListAssetConfigsResponse { asset_configs } = + t.query(&QueryMsg::ListAssetConfigs {}).unwrap(); + + assert_eq!(asset_configs, expected_asset_configs); + + // list asset groups + let ListAssetGroupsResponse { asset_groups } = t.query(&QueryMsg::ListAssetGroups {}).unwrap(); + + assert_eq!(asset_groups, BTreeMap::new()); +} + fn get_prev_version_of_wasm_byte_code(version: &str) -> Vec { let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let wasm_path = manifest_path From 2d8d3c7cfbf902624a95e99fe754b5aac20f467e Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Mon, 7 Oct 2024 14:10:07 +0700 Subject: [PATCH 18/26] integration test for asset group limiter --- .../src/test/cases/units/swap/mod.rs | 17 +-- .../swap/swap_with_asset_group_limiters.rs | 100 ++++++++++++++++++ 2 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 contracts/transmuter/src/test/cases/units/swap/swap_with_asset_group_limiters.rs diff --git a/contracts/transmuter/src/test/cases/units/swap/mod.rs b/contracts/transmuter/src/test/cases/units/swap/mod.rs index ed93f62..bd954e7 100644 --- a/contracts/transmuter/src/test/cases/units/swap/mod.rs +++ b/contracts/transmuter/src/test/cases/units/swap/mod.rs @@ -27,14 +27,14 @@ macro_rules! test_swap { #[test] fn $test_name() { let app = osmosis_test_tube::OsmosisTestApp::new(); - test_swap_success_case($setup(&app), $msg, $received); + test_swap_success_case(&$setup(&app), $msg, $received); } }; ($test_name:ident [expect error] { setup = $setup:ident, msg = $msg:expr, err = $err:expr }) => { #[test] fn $test_name() { let app = osmosis_test_tube::OsmosisTestApp::new(); - test_swap_failed_case($setup(&app), $msg, $err); + test_swap_failed_case(&$setup(&app), $msg, $err); } }; ($test_name:ident [expect ok] { setup = $setup:expr, msgs = $msgs:expr, received = $received:expr }) => { @@ -42,7 +42,7 @@ macro_rules! test_swap { fn $test_name() { for msg in $msgs { let app = osmosis_test_tube::OsmosisTestApp::new(); - test_swap_success_case($setup(&app), msg, $received); + test_swap_success_case(&$setup(&app), msg, $received); } } }; @@ -51,7 +51,7 @@ macro_rules! test_swap { fn $test_name() { for msg in $msgs { let app = osmosis_test_tube::OsmosisTestApp::new(); - test_swap_failed_case($setup(&app), msg, $err); + test_swap_failed_case(&$setup(&app), msg, $err); } } }; @@ -71,7 +71,7 @@ pub enum SwapMsg { }, } -fn assert_invariants(t: TestEnv, act: impl FnOnce(&TestEnv) -> String) { +fn assert_invariants(t: &TestEnv, act: impl FnOnce(&TestEnv) -> String) { // store previous shares and pool assets let prev_shares = t .contract @@ -159,7 +159,7 @@ fn lcm_normalization_factor(configs: &[AssetConfig]) -> Uint128 { lcm_from_iter(norm_factors).unwrap() } -fn test_swap_success_case(t: TestEnv, msg: SwapMsg, received: Coin) { +fn test_swap_success_case(t: &TestEnv, msg: SwapMsg, received: Coin) { assert_invariants(t, move |t| { let cp = CosmwasmPool::new(t.app); let bank = Bank::new(t.app); @@ -422,7 +422,7 @@ pub fn test_swap_share_denom_success_case(t: &TestEnv, msg: SwapMsg, sent: Coin, ); } -fn test_swap_failed_case(t: TestEnv, msg: SwapMsg, err: ContractError) { +fn test_swap_failed_case(t: &TestEnv, msg: SwapMsg, err: ContractError) { assert_invariants(t, move |t| { let cp = CosmwasmPool::new(t.app); @@ -488,6 +488,7 @@ fn pool_with_single_lp( .collect::>(); let t = TestEnvBuilder::new() + .with_account("admin", non_zero_pool_assets.clone()) .with_account("provider", non_zero_pool_assets.clone()) .with_account( SWAPPER, @@ -531,3 +532,5 @@ fn pool_with_single_lp( mod client_error; mod non_empty_pool; mod swap_share_denom; + +mod swap_with_asset_group_limiters; diff --git a/contracts/transmuter/src/test/cases/units/swap/swap_with_asset_group_limiters.rs b/contracts/transmuter/src/test/cases/units/swap/swap_with_asset_group_limiters.rs new file mode 100644 index 0000000..1842b75 --- /dev/null +++ b/contracts/transmuter/src/test/cases/units/swap/swap_with_asset_group_limiters.rs @@ -0,0 +1,100 @@ +use cosmwasm_std::{Coin, Decimal, Uint128, Uint64}; + +use crate::{ + asset::AssetConfig, + contract::sv::ExecMsg, + limiter::{LimiterParams, WindowConfig}, + scope::Scope, + ContractError, +}; + +use super::{pool_with_single_lp, test_swap_failed_case, test_swap_success_case, SwapMsg}; + +const REMAINING_DENOM0: u128 = 1_000_000_000_000; +const REMAINING_DENOM1: u128 = 1_000_000_000_000; +const REMAINING_DENOM2: u128 = 1_000_000_000_000; + +#[test] +fn test_swap_with_asset_group_limiters() { + let app = osmosis_test_tube::OsmosisTestApp::new(); + let t = pool_with_single_lp( + &app, + vec![ + Coin::new(REMAINING_DENOM0, "denom0"), + Coin::new(REMAINING_DENOM1, "denom1"), + Coin::new(REMAINING_DENOM2, "denom2"), + ], + vec![ + AssetConfig { + denom: "denom0".to_string(), + normalization_factor: Uint128::one(), + }, + AssetConfig { + denom: "denom1".to_string(), + normalization_factor: Uint128::one(), + }, + AssetConfig { + denom: "denom2".to_string(), + normalization_factor: Uint128::one(), + }, + ], + ); + + // Add asset group and limiters + t.contract + .execute( + &ExecMsg::CreateAssetGroup { + label: "group1".to_string(), + denoms: vec!["denom0".to_string(), "denom1".to_string()], + }, + &[], + &t.accounts["admin"], + ) + .unwrap(); + + t.contract + .execute( + &ExecMsg::RegisterLimiter { + scope: Scope::asset_group("group1"), + label: "limiter1".to_string(), + limiter_params: LimiterParams::ChangeLimiter { + window_config: WindowConfig { + window_size: Uint64::from(1000u64), + division_count: Uint64::from(5u64), + }, + boundary_offset: Decimal::percent(10), + }, + }, + &[], + &t.accounts["admin"], + ) + .unwrap(); + + // swap within group, even an agressive one wouldn't effect anything + test_swap_success_case( + &t, + SwapMsg::SwapExactAmountOut { + token_in_denom: "denom0".to_string(), + token_in_max_amount: Uint128::from(REMAINING_DENOM1), + token_out: Coin::new(REMAINING_DENOM1, "denom1".to_string()), + }, + Coin::new(REMAINING_DENOM1, "denom1".to_string()), + ); + + app.increase_time(5); + + // swap denom0 to denom2 -> increase group1 weight by adding more denom0 + test_swap_failed_case( + &t, + SwapMsg::SwapExactAmountOut { + token_in_denom: "denom0".to_string(), + token_in_max_amount: Uint128::from(REMAINING_DENOM0), + token_out: Coin::new(REMAINING_DENOM0, "denom2".to_string()), + }, + ContractError::UpperLimitExceeded { + scope: Scope::asset_group("group1"), + upper_limit: "0.766666666666666666".parse().unwrap(), + value: Decimal::percent(100), + }, + ); +} From 31cad52364fe4dcdaec7fd9d89ccbbe126b78032 Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Tue, 22 Oct 2024 15:54:26 +0700 Subject: [PATCH 19/26] update deps --- Cargo.lock | 902 +++++++++++++----- Cargo.toml | 4 +- contracts/transmuter/Cargo.toml | 17 +- contracts/transmuter/src/alloyed_asset.rs | 28 +- contracts/transmuter/src/asset.rs | 6 +- contracts/transmuter/src/contract.rs | 896 +++++++++-------- contracts/transmuter/src/lib.rs | 2 +- contracts/transmuter/src/limiter.rs | 12 +- contracts/transmuter/src/math.rs | 2 +- contracts/transmuter/src/migrations/v4_0_0.rs | 7 +- contracts/transmuter/src/role/admin.rs | 8 +- contracts/transmuter/src/role/mod.rs | 10 +- contracts/transmuter/src/role/moderator.rs | 8 +- contracts/transmuter/src/sudo.rs | 111 ++- contracts/transmuter/src/swap.rs | 156 +-- .../transmuter/src/test/cases/scenarios.rs | 271 +++--- .../src/test/cases/units/add_new_assets.rs | 18 +- .../transmuter/src/test/cases/units/admin.rs | 11 +- .../src/test/cases/units/create_pool.rs | 9 +- .../src/test/cases/units/join_and_exit.rs | 128 ++- .../src/test/cases/units/migrate.rs | 23 +- .../src/test/cases/units/spot_price.rs | 24 +- .../src/test/cases/units/swap/client_error.rs | 18 +- .../src/test/cases/units/swap/mod.rs | 4 +- .../test/cases/units/swap/non_empty_pool.rs | 60 +- .../test/cases/units/swap/swap_share_denom.rs | 94 +- .../swap/swap_with_asset_group_limiters.rs | 14 +- contracts/transmuter/src/test/test_env.rs | 14 +- .../src/transmuter_pool/add_new_assets.rs | 72 +- .../src/transmuter_pool/asset_group.rs | 35 +- .../src/transmuter_pool/corrupted_assets.rs | 56 +- .../src/transmuter_pool/exit_pool.rs | 59 +- .../src/transmuter_pool/join_pool.rs | 40 +- .../src/transmuter_pool/transmute.rs | 106 +- .../transmuter/src/transmuter_pool/weight.rs | 6 +- 35 files changed, 1867 insertions(+), 1364 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 56dbf61..ce50a47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,18 @@ dependencies = [ "version_check", ] +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.0.5" @@ -37,12 +49,139 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "anyhow" version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "ark-bls12-381" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c775f0d12169cba7aae4caeb547bb6a50781c7449a8aa53793827c9ec4abf488" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-ec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" +dependencies = [ + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", + "itertools 0.10.5", + "num-traits", + "rayon", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint", + "num-traits", + "paste", + "rayon", + "rustc_version", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-poly" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" +dependencies = [ + "ark-ff", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand", + "rayon", +] + [[package]] name = "async-trait" version = "0.1.73" @@ -51,7 +190,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.82", ] [[package]] @@ -87,6 +226,12 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -99,27 +244,30 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bech32" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" + [[package]] name = "bindgen" -version = "0.69.1" +version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ffcebc3849946a7170a05992aac39da343a90676ab392c51a4280981d6379c2" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ "bitflags 2.4.1", "cexpr", "clang-sys", - "lazy_static", - "lazycell", + "itertools 0.13.0", "log", - "peeking_take_while", "prettyplease", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", - "syn 2.0.60", - "which", + "syn 2.0.82", ] [[package]] @@ -174,6 +322,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56953345e39537a3e18bdaeba4cb0c58a78c1f61f361dc0fa7c5c7340ae87c5f" +[[package]] +name = "bnum" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e31ea183f6ee62ac8b8a8cf7feddd766317adfb13ff469de57ce033efd6a790" + [[package]] name = "bs58" version = "0.5.0" @@ -287,20 +441,19 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cosmos-sdk-proto" -version = "0.20.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32560304ab4c365791fd307282f76637213d8083c1a98490c35159cd67852237" +checksum = "e8ce7f4797cdf5cd18be6555ff3f0a8d37023c2d60f3b2708895d601b85c1c46" dependencies = [ - "prost 0.12.3", - "prost-types 0.12.3", + "prost 0.13.3", "tendermint-proto", ] [[package]] name = "cosmrs" -version = "0.15.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47126f5364df9387b9d8559dcef62e99010e1d4098f39eb3f7ee4b5c254e40ea" +checksum = "09f90935b72d9fa65a2a784e09f25778637b7e88e9d6f87c717081470f7fa726" dependencies = [ "bip32", "cosmos-sdk-proto", @@ -317,34 +470,73 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cosmwasm-core" +version = "2.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6ceb8624260d0d3a67c4e1a1d43fc7e9406720afbcb124521501dd138f90aa" + +[[package]] +name = "cosmwasm-crypto" +version = "1.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58535cbcd599b3c193e3967c8292fe1dbbb5de7c2a2d87380661091dd4744044" +dependencies = [ + "digest 0.10.7", + "ed25519-zebra 3.1.0", + "k256", + "rand_core 0.6.4", + "thiserror", +] + [[package]] name = "cosmwasm-crypto" -version = "1.5.4" +version = "2.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b4c3f9c4616d6413d4b5fc4c270a4cc32a374b9be08671e80e1a019f805d8f" +checksum = "4125381e5fd7fefe9f614640049648088015eca2b60d861465329a5d87dfa538" dependencies = [ + "ark-bls12-381", + "ark-ec", + "ark-ff", + "ark-serialize", + "cosmwasm-core", "digest 0.10.7", "ecdsa", - "ed25519-zebra", + "ed25519-zebra 4.0.3", "k256", + "num-traits", + "p256", "rand_core 0.6.4", + "rayon", + "sha2 0.10.7", "thiserror", ] [[package]] name = "cosmwasm-derive" -version = "1.5.4" +version = "1.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c586ced10c3b00e809ee664a895025a024f60d65d34fe4c09daed4a4db68a3f3" +checksum = "a8e07de16c800ac82fd188d055ecdb923ead0cf33960d3350089260bb982c09f" dependencies = [ "syn 1.0.109", ] +[[package]] +name = "cosmwasm-derive" +version = "2.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5658b1dc64e10b56ae7a449f678f96932a96f6cfad1769d608d1d1d656480a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.82", +] + [[package]] name = "cosmwasm-schema" -version = "1.5.4" +version = "2.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8467874827d384c131955ff6f4d47d02e72a956a08eb3c0ff24f8c903a5517b4" +checksum = "f86b4d949b6041519c58993a73f4bbfba8083ba14f7001eae704865a09065845" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -355,26 +547,26 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.5.4" +version = "2.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6db85d98ac80922aef465e564d5b21fa9cfac5058cb62df7f116c3682337393" +checksum = "c8ef1b5835a65fcca3ab8b9a02b4f4dacc78e233a5c2f20b270efb9db0666d12" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.82", ] [[package]] name = "cosmwasm-std" -version = "1.5.4" +version = "1.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712fe58f39d55c812f7b2c84e097cdede3a39d520f89b6dc3153837e31741927" +checksum = "c21fde95ccd20044a23c0ac6fd8c941f3e8c158169dc94b5aa6491a2d9551a8d" dependencies = [ - "base64", - "bech32", - "bnum", - "cosmwasm-crypto", - "cosmwasm-derive", + "base64 0.21.5", + "bech32 0.9.1", + "bnum 0.10.0", + "cosmwasm-crypto 1.5.8", + "cosmwasm-derive 1.5.8", "derivative", "forward_ref", "hex", @@ -386,13 +578,36 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cosmwasm-std" +version = "2.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70eb7ab0c1e99dd6207496963ba2a457c4128ac9ad9c72a83f8d9808542b849b" +dependencies = [ + "base64 0.22.1", + "bech32 0.11.0", + "bnum 0.11.0", + "cosmwasm-core", + "cosmwasm-crypto 2.1.4", + "cosmwasm-derive 2.1.4", + "derive_more", + "hex", + "rand_core 0.6.4", + "schemars", + "serde", + "serde-json-wasm 1.0.1", + "sha2 0.10.7", + "static_assertions", + "thiserror", +] + [[package]] name = "cosmwasm-storage" version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b52be0d56b78f502f3acb75e40908a0d04d9f265b6beb0f86b36ec2ece048748" dependencies = [ - "cosmwasm-std", + "cosmwasm-std 1.5.8", "serde", ] @@ -405,6 +620,31 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + [[package]] name = "crypto-bigint" version = "0.5.3" @@ -440,6 +680,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.82", +] + [[package]] name = "curve25519-dalek-ng" version = "4.1.1" @@ -455,25 +722,26 @@ dependencies = [ [[package]] name = "cw-storage-plus" -version = "1.1.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f0e92a069d62067f3472c62e30adedb4cab1754725c0f2a682b3128d2bf3c79" +checksum = "f13360e9007f51998d42b1bc6b7fa0141f74feae61ed5fd1e5b0a89eec7b5de1" dependencies = [ - "cosmwasm-std", + "cosmwasm-std 2.1.4", "schemars", "serde", ] [[package]] name = "cw2" -version = "1.1.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ac2dc7a55ad64173ca1e0a46697c31b7a5c51342f55a1e84a724da4eb99908" +checksum = "b04852cd38f044c0751259d5f78255d07590d136b8a86d4e09efdd7666bd6d27" dependencies = [ "cosmwasm-schema", - "cosmwasm-std", + "cosmwasm-std 2.1.4", "cw-storage-plus", "schemars", + "semver", "serde", "thiserror", ] @@ -499,6 +767,27 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.82", + "unicode-xid", +] + [[package]] name = "digest" version = "0.9.0" @@ -569,7 +858,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 3.2.0", "hashbrown 0.12.3", "hex", "rand_core 0.6.4", @@ -578,6 +867,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed25519-zebra" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d9ce6874da5d4415896cd45ffbc4d1cfc0c4f9c079427bd870742c30f2f65a9" +dependencies = [ + "curve25519-dalek 4.1.3", + "ed25519", + "hashbrown 0.14.0", + "hex", + "rand_core 0.6.4", + "sha2 0.10.7", + "zeroize", +] + [[package]] name = "either" version = "1.9.0" @@ -638,6 +942,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "flex-error" version = "0.4.4" @@ -671,9 +981,9 @@ checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" [[package]] name = "futures" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -686,9 +996,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -696,15 +1006,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -713,44 +1023,44 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.82", ] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-timer" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -836,7 +1146,16 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash", + "ahash 0.7.6", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.11", ] [[package]] @@ -844,6 +1163,16 @@ name = "hashbrown" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +dependencies = [ + "ahash 0.8.11", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" [[package]] name = "hermit-abi" @@ -966,12 +1295,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown 0.15.0", ] [[package]] @@ -991,18 +1320,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -1024,9 +1344,9 @@ dependencies = [ [[package]] name = "k256" -version = "0.13.1" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ "cfg-if", "ecdsa", @@ -1038,9 +1358,9 @@ dependencies = [ [[package]] name = "konst" -version = "0.3.6" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030400e39b2dff8beaa55986a17e0014ad657f569ca92426aafcb5e8e71faee7" +checksum = "50a0ba6de5f7af397afff922f22c149ff605c766cd3269cf6c1cd5e466dbe3b9" dependencies = [ "const_panic", "konst_kernel", @@ -1050,9 +1370,9 @@ dependencies = [ [[package]] name = "konst_kernel" -version = "0.3.6" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3376133edc39f027d551eb77b077c2865a0ef252b2e7d0dd6b6dc303db95d8b5" +checksum = "be0a455a1719220fd6adf756088e1c69a85bf14b6a9e24537a5cc04f503edb2b" dependencies = [ "typewit", ] @@ -1063,18 +1383,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e28ab1dc35e09d60c2b8c90d12a9a8d9666c876c10a3739a3196db0103b6043" -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libc" version = "0.2.151" @@ -1146,21 +1454,29 @@ dependencies = [ ] [[package]] -name = "num-derive" -version = "0.3.3" +name = "num-bigint" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", ] [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] @@ -1213,15 +1529,15 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "osmosis-std" -version = "0.22.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8641c376f01f5af329dc2a34e4f5527eaeb0bde18cda8d86fed958d04c86159c" +checksum = "cd621cc2f26474c6fb689ccc114dc0d8b53369a322f1fa5f24802f3de3d3def3" dependencies = [ "chrono", - "cosmwasm-std", + "cosmwasm-std 2.1.4", "osmosis-std-derive", - "prost 0.12.3", - "prost-types 0.12.3", + "prost 0.13.3", + "prost-types 0.13.3", "schemars", "serde", "serde-cw-value", @@ -1229,9 +1545,9 @@ dependencies = [ [[package]] name = "osmosis-std-derive" -version = "0.20.1" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5ebdfd1bc8ed04db596e110c6baa9b174b04f6ed1ec22c666ddc5cb3fa91bd7" +checksum = "8b0240fd030a4bbc79fa6cbea0b3eb0260a4b79075ebc039b93e2652bff8655b" dependencies = [ "itertools 0.10.5", "proc-macro2", @@ -1242,16 +1558,14 @@ dependencies = [ [[package]] name = "osmosis-test-tube" -version = "22.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a082b97136d15470a37aa758f227c865594590b69d74721248ed5adf59bf1ca2" +version = "26.0.1" dependencies = [ - "base64", + "base64 0.22.1", "bindgen", "cosmrs", - "cosmwasm-std", + "cosmwasm-std 2.1.4", "osmosis-std", - "prost 0.12.3", + "prost 0.13.3", "serde", "serde_json", "test-tube", @@ -1259,22 +1573,28 @@ dependencies = [ ] [[package]] -name = "paste" -version = "1.0.14" +name = "p256" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.7", +] [[package]] -name = "peeking_take_while" -version = "0.1.2" +name = "paste" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "peg" -version = "0.7.0" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c0b841ea54f523f7aa556956fbd293bcbe06f2e67d2eb732b7278aaf1d166a" +checksum = "295283b02df346d1ef66052a757869b2876ac29a6bb0ac3f5f7cd44aebe40e8f" dependencies = [ "peg-macros", "peg-runtime", @@ -1282,9 +1602,9 @@ dependencies = [ [[package]] name = "peg-macros" -version = "0.7.0" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aa52829b8decbef693af90202711348ab001456803ba2a98eb4ec8fb70844c" +checksum = "bdad6a1d9cf116a059582ce415d5f5566aabcd4008646779dab7fdc2a9a9d426" dependencies = [ "peg-runtime", "proc-macro2", @@ -1293,9 +1613,9 @@ dependencies = [ [[package]] name = "peg-runtime" -version = "0.7.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c719dcf55f09a3a7e764c6649ab594c18a177e3599c467983cdf644bfc0a4088" +checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a" [[package]] name = "percent-encoding" @@ -1320,7 +1640,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.82", ] [[package]] @@ -1345,6 +1665,15 @@ dependencies = [ "spki", ] +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + [[package]] name = "prettyplease" version = "0.2.15" @@ -1352,17 +1681,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.60", + "syn 2.0.82", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", ] [[package]] name = "proc-macro-crate" -version = "1.3.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ - "once_cell", - "toml_edit", + "toml_edit 0.22.22", ] [[package]] @@ -1391,9 +1728,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" dependencies = [ "unicode-ident", ] @@ -1410,12 +1747,12 @@ dependencies = [ [[package]] name = "prost" -version = "0.12.3" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f" dependencies = [ "bytes", - "prost-derive 0.12.3", + "prost-derive 0.13.3", ] [[package]] @@ -1433,15 +1770,15 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.3" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.13.0", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.82", ] [[package]] @@ -1455,22 +1792,43 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.12.3" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +checksum = "4759aa0d3a6232fb8dbdb97b61de2c20047c68aca932c7ed76da9d788508d670" dependencies = [ - "prost 0.12.3", + "prost 0.13.3", ] [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -1486,11 +1844,31 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "regex" -version = "1.9.4" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" dependencies = [ "aho-corasick", "memchr", @@ -1500,9 +1878,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", @@ -1511,15 +1889,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.5" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "relative-path" -version = "1.9.0" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c707298afce11da2efef2f600116fa93ffa7a032b5d7b628aa17711ec81383ca" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "reqwest" @@ -1527,7 +1905,7 @@ version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" dependencies = [ - "base64", + "base64 0.21.5", "bytes", "encoding_rs", "futures-core", @@ -1596,9 +1974,9 @@ dependencies = [ [[package]] name = "rstest" -version = "0.18.2" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +checksum = "0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035" dependencies = [ "futures", "futures-timer", @@ -1608,18 +1986,19 @@ dependencies = [ [[package]] name = "rstest_macros" -version = "0.18.2" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +checksum = "825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a" dependencies = [ "cfg-if", "glob", + "proc-macro-crate", "proc-macro2", "quote", "regex", "relative-path", "rustc_version", - "syn 2.0.60", + "syn 2.0.82", "unicode-ident", ] @@ -1637,9 +2016,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] @@ -1674,7 +2053,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64", + "base64 0.21.5", ] [[package]] @@ -1713,9 +2092,9 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.13" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763f8cd0d4c71ed8389c90cb8100cba87e763bd01a8e614d4f0af97bcd50a161" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" dependencies = [ "dyn-clone", "schemars_derive", @@ -1725,14 +2104,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.13" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0f696e21e10fa546b7ffb1c9672c6de8fbc7a81acf59524386d8639bf12737" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 1.0.109", + "syn 2.0.82", ] [[package]] @@ -1790,9 +2169,9 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.188" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] @@ -1835,24 +2214,24 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.82", ] [[package]] name = "serde_derive_internals" -version = "0.26.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.82", ] [[package]] @@ -1874,7 +2253,16 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.82", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", ] [[package]] @@ -2003,12 +2391,13 @@ checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" [[package]] name = "sylvia" -version = "0.10.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64c8e892525ec035e5bdbeceec19309b1534734da7ff7bc69e9f639b077c497" +checksum = "96e34c4bc8b8847011ca9c1478cfd5f532acb691377bca5fecf748833acffde8" dependencies = [ "cosmwasm-schema", - "cosmwasm-std", + "cosmwasm-std 2.1.4", + "derivative", "konst", "schemars", "serde", @@ -2019,17 +2408,17 @@ dependencies = [ [[package]] name = "sylvia-derive" -version = "0.10.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae5e0f41752efba2c6895514fa4caae742f69a2a73b4790bb6e365400c563bc1" +checksum = "ad349313e23cfe1d54876be195a31b3293ca91ae31754abcf1f908e10812b4e7" dependencies = [ "convert_case", - "itertools 0.12.0", + "itertools 0.13.0", "proc-macro-crate", "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.82", ] [[package]] @@ -2045,9 +2434,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.60" +version = "2.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021" dependencies = [ "proc-macro2", "quote", @@ -2077,9 +2466,9 @@ dependencies = [ [[package]] name = "tendermint" -version = "0.34.0" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc2294fa667c8b548ee27a9ba59115472d0a09c2ba255771092a7f1dcf03a789" +checksum = "2f3afea7809ffaaf1e5d9c3c9997cb3a834df7e94fbfab2fad2bc4577f1cde41" dependencies = [ "bytes", "digest 0.10.7", @@ -2090,8 +2479,7 @@ dependencies = [ "k256", "num-traits", "once_cell", - "prost 0.12.3", - "prost-types 0.12.3", + "prost 0.13.3", "ripemd", "serde", "serde_bytes", @@ -2108,9 +2496,9 @@ dependencies = [ [[package]] name = "tendermint-config" -version = "0.34.0" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a25dbe8b953e80f3d61789fbdb83bf9ad6c0ef16df5ca6546f49912542cc137" +checksum = "d8add7b85b0282e5901521f78fe441956ac1e2752452f4e1f2c0ce7e1f10d485" dependencies = [ "flex-error", "serde", @@ -2122,16 +2510,13 @@ dependencies = [ [[package]] name = "tendermint-proto" -version = "0.34.0" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc728a4f9e891d71adf66af6ecaece146f9c7a11312288a3107b3e1d6979aaf" +checksum = "bf3abf34ecf33125621519e9952688e7a59a98232d51538037ba21fbe526a802" dependencies = [ "bytes", "flex-error", - "num-derive", - "num-traits", - "prost 0.12.3", - "prost-types 0.12.3", + "prost 0.13.3", "serde", "serde_bytes", "subtle-encoding", @@ -2140,9 +2525,9 @@ dependencies = [ [[package]] name = "tendermint-rpc" -version = "0.34.0" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbf0a4753b46a190f367337e0163d0b552a2674a6bac54e74f9f2cdcde2969b" +checksum = "9693f42544bf3b41be3cbbfa418650c86e137fb8f5a57981659a84b677721ecf" dependencies = [ "async-trait", "bytes", @@ -2151,6 +2536,7 @@ dependencies = [ "getrandom", "peg", "pin-project", + "rand", "reqwest", "semver", "serde", @@ -2172,15 +2558,13 @@ dependencies = [ [[package]] name = "test-tube" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09184c7655b2bdaf4517b06141a2e4c44360904f2706a05b24c831bd97ad1db6" +version = "0.8.0" dependencies = [ - "base64", + "base64 0.22.1", "cosmrs", - "cosmwasm-std", + "cosmwasm-std 2.1.4", "osmosis-std", - "prost 0.12.3", + "prost 0.13.3", "serde", "serde_json", "thiserror", @@ -2203,7 +2587,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.82", ] [[package]] @@ -2263,7 +2647,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.82", ] [[package]] @@ -2292,28 +2676,47 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.11" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" dependencies = [ "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.20.2", ] [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" -version = "0.19.14" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.0.0", + "indexmap 2.6.0", + "serde", + "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.5.15", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap 2.6.0", + "toml_datetime", + "winnow 0.6.20", ] [[package]] @@ -2347,11 +2750,11 @@ name = "transmuter" version = "4.0.0" dependencies = [ "cosmwasm-schema", - "cosmwasm-std", + "cosmwasm-std 2.1.4", "cosmwasm-storage", "cw-storage-plus", "cw2", - "itertools 0.12.0", + "itertools 0.13.0", "osmosis-std", "osmosis-test-tube", "rstest", @@ -2367,7 +2770,7 @@ name = "transmuter_math" version = "1.0.0" dependencies = [ "cosmwasm-schema", - "cosmwasm-std", + "cosmwasm-std 2.1.4", "thiserror", ] @@ -2397,9 +2800,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-normalization" @@ -2416,6 +2819,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "untrusted" version = "0.9.0" @@ -2435,9 +2844,9 @@ dependencies = [ [[package]] name = "uuid" -version = "0.8.2" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" [[package]] name = "version_check" @@ -2491,7 +2900,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.82", "wasm-bindgen-shared", ] @@ -2525,7 +2934,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.82", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2546,17 +2955,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "which" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" -dependencies = [ - "either", - "libc", - "once_cell", -] - [[package]] name = "winapi" version = "0.3.9" @@ -2663,6 +3061,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" @@ -2673,6 +3080,27 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.82", +] + [[package]] name = "zeroize" version = "1.7.0" @@ -2690,5 +3118,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.82", ] diff --git a/Cargo.toml b/Cargo.toml index a64a38e..2a43f4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,8 @@ members = [ ] [workspace.dependencies] -cosmwasm-schema = "1.3.1" -cosmwasm-std = {version = "1.5.4"} +cosmwasm-schema = "2.1" +cosmwasm-std = {version = "2.1"} [profile.release] codegen-units = 1 diff --git a/contracts/transmuter/Cargo.toml b/contracts/transmuter/Cargo.toml index 51693b2..e64bf21 100644 --- a/contracts/transmuter/Cargo.toml +++ b/contracts/transmuter/Cargo.toml @@ -29,8 +29,7 @@ rpath = false [features] # skip integration test for cases like running `cargo mutants` skip-integration-test = [] -# for more explicit tests, cargo test --features=backtraces -backtraces = ["cosmwasm-std/backtraces"] + # use library feature to disable all instantiate/execute/query exports library = [] @@ -45,19 +44,19 @@ optimize = """docker run --rm -v "$(pwd)":/code \ cosmwasm-schema = {workspace = true} cosmwasm-std = {workspace = true, features = ["cosmwasm_1_1"]} cosmwasm-storage = "1.3.1" -cw-storage-plus = "1.1.0" -cw2 = "1.1.0" -osmosis-std = "0.22.0" +cw-storage-plus = "2.0.0" +cw2 = "2.0.0" +osmosis-std = "0.26.0" schemars = "0.8.12" serde = {version = "1.0.183", default-features = false, features = ["derive"]} -sylvia = "0.10.1" +sylvia = "1.2.1" thiserror = {version = "1.0.44"} transmuter_math = {version = "1.0.0", path = "../../packages/transmuter_math"} [dev-dependencies] -itertools = "0.12.0" -osmosis-test-tube = "22.1.0" -rstest = "0.18.2" +itertools = "0.13.0" +osmosis-test-tube = {path = "../../../test-tube/packages/osmosis-test-tube"} +rstest = "0.23.0" [lints.rust] unexpected_cfgs = {level = "warn", check-cfg = [ diff --git a/contracts/transmuter/src/alloyed_asset.rs b/contracts/transmuter/src/alloyed_asset.rs index 2d87466..e1ad5dc 100644 --- a/contracts/transmuter/src/alloyed_asset.rs +++ b/contracts/transmuter/src/alloyed_asset.rs @@ -10,15 +10,15 @@ use crate::{ /// and since the pool is a 1:1 multi-asset pool, it act /// as a composite of the underlying assets and assume 1:1 /// value to the underlying assets. -pub struct AlloyedAsset<'a> { - alloyed_denom: Item<'a, String>, - normalization_factor: Item<'a, Uint128>, +pub struct AlloyedAsset { + alloyed_denom: Item, + normalization_factor: Item, } -impl<'a> AlloyedAsset<'a> { +impl AlloyedAsset { pub const fn new( - alloyed_denom_namespace: &'a str, - normalization_factor_namespace: &'a str, + alloyed_denom_namespace: &'static str, + normalization_factor_namespace: &'static str, ) -> Self { Self { alloyed_denom: Item::new(alloyed_denom_namespace), @@ -211,7 +211,7 @@ pub mod swap_from_alloyed { #[cfg(test)] mod tests { - use cosmwasm_std::testing::mock_dependencies; + use cosmwasm_std::{coin, testing::mock_dependencies}; use super::*; @@ -226,7 +226,7 @@ mod tests { .set_alloyed_denom(&mut deps.storage, &alloyed_denom) .unwrap(); - deps.querier.update_balance( + deps.querier.bank.update_balance( "osmo1addr1", vec![Coin { denom: alloyed_denom.clone(), @@ -234,7 +234,7 @@ mod tests { }], ); - deps.querier.update_balance( + deps.querier.bank.update_balance( "osmo1addr2", vec![Coin { denom: alloyed_denom.clone(), @@ -275,7 +275,7 @@ mod tests { // same normalization factor let amount = AlloyedAsset::amount_from( - &[(Coin::new(100, "ua"), Uint128::one())], + &[(coin(100, "ua"), Uint128::one())], Uint128::one(), Rounding::Up, ) @@ -286,8 +286,8 @@ mod tests { // different normalization factor let amount = AlloyedAsset::amount_from( &[ - (Coin::new(100, "ua"), Uint128::from(2u128)), - (Coin::new(100, "ub"), Uint128::from(3u128)), + (coin(100, "ua"), Uint128::from(2u128)), + (coin(100, "ub"), Uint128::from(3u128)), ], Uint128::one(), Rounding::Up, @@ -297,8 +297,8 @@ mod tests { let amount = AlloyedAsset::amount_from( &[ - (Coin::new(100, "ua"), Uint128::from(2u128)), - (Coin::new(100, "ub"), Uint128::from(3u128)), + (coin(100, "ua"), Uint128::from(2u128)), + (coin(100, "ub"), Uint128::from(3u128)), ], Uint128::one(), Rounding::Down, diff --git a/contracts/transmuter/src/asset.rs b/contracts/transmuter/src/asset.rs index 9d0db46..41fb1c6 100644 --- a/contracts/transmuter/src/asset.rs +++ b/contracts/transmuter/src/asset.rs @@ -221,7 +221,7 @@ pub fn convert_amount( #[cfg(test)] mod tests { use super::*; - use cosmwasm_std::{testing::mock_dependencies_with_balances, Coin}; + use cosmwasm_std::{coin, testing::mock_dependencies_with_balances}; #[test] fn test_convert_amount() { @@ -353,8 +353,8 @@ mod tests { #[test] fn test_checked_init_asset() { let deps = mock_dependencies_with_balances(&[ - ("addr1", &[Coin::new(1, "denom1")]), - ("addr2", &[Coin::new(1, "denom2")]), + ("addr1", &[coin(1, "denom1")]), + ("addr2", &[coin(1, "denom2")]), ]); // denom1 diff --git a/contracts/transmuter/src/contract.rs b/contracts/transmuter/src/contract.rs index 2e7cd0e..4b035e2 100644 --- a/contracts/transmuter/src/contract.rs +++ b/contracts/transmuter/src/contract.rs @@ -38,12 +38,12 @@ const CREATE_ALLOYED_DENOM_REPLY_ID: u64 = 1; /// Prefix for alloyed asset denom const ALLOYED_PREFIX: &str = "alloyed"; -pub struct Transmuter<'a> { - pub(crate) active_status: Item<'a, bool>, - pub(crate) pool: Item<'a, TransmuterPool>, - pub(crate) alloyed_asset: AlloyedAsset<'a>, - pub(crate) role: Role<'a>, - pub(crate) limiters: Limiters<'a>, +pub struct Transmuter { + pub(crate) active_status: Item, + pub(crate) pool: Item, + pub(crate) alloyed_asset: AlloyedAsset, + pub(crate) role: Role, + pub(crate) limiters: Limiters, } pub mod key { @@ -58,9 +58,9 @@ pub mod key { #[contract] #[sv::error(ContractError)] -impl Transmuter<'_> { +impl Transmuter { /// Create a Transmuter instance. - pub const fn default() -> Self { + pub const fn new() -> Self { Self { active_status: Item::new(key::ACTIVE_STATUS), pool: Item::new(key::POOL), @@ -1030,9 +1030,10 @@ mod tests { use crate::sudo::SudoMsg; use crate::*; - use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; + use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env}; use cosmwasm_std::{ - attr, from_json, BankMsg, BlockInfo, Storage, SubMsgResponse, SubMsgResult, Uint64, + attr, coin, from_json, BankMsg, Binary, BlockInfo, Storage, SubMsgResponse, SubMsgResult, + Uint64, }; use osmosis_std::types::osmosis::tokenfactory::v1beta1::MsgBurn; @@ -1042,10 +1043,11 @@ mod tests { // make denom has non-zero total supply deps.querier - .update_balance("someone", vec![Coin::new(1, "tbtc"), Coin::new(1, "nbtc")]); + .bank + .update_balance("someone", vec![coin(1, "tbtc"), coin(1, "nbtc")]); - let admin = "admin"; - let moderator = "moderator"; + let admin = deps.api.addr_make("admin"); + let moderator = deps.api.addr_make("moderator"); let init_msg = InstantiateMsg { pool_asset_configs: vec![ AssetConfig::from_denom_str("tbtc"), @@ -1057,7 +1059,7 @@ mod tests { moderator: moderator.to_string(), }; let env = mock_env(); - let info = mock_info(admin, &[]); + let info = message_info(&admin, &[]); // Instantiate the contract. let err = instantiate(deps.as_mut(), env.clone(), info.clone(), init_msg).unwrap_err(); @@ -1074,19 +1076,21 @@ mod tests { fn test_add_new_assets() { let mut deps = mock_dependencies(); + let someone = deps.api.addr_make("someone"); + // make denom has non-zero total supply - deps.querier.update_balance( - "someone", + deps.querier.bank.update_balance( + someone, vec![ - Coin::new(1, "uosmo"), - Coin::new(1, "uion"), - Coin::new(1, "new_asset1"), - Coin::new(1, "new_asset2"), + coin(1, "uosmo"), + coin(1, "uion"), + coin(1, "new_asset1"), + coin(1, "new_asset2"), ], ); - let admin = "admin"; - let moderator = "moderator"; + let admin = deps.api.addr_make("admin"); + let moderator = deps.api.addr_make("moderator"); let init_msg = InstantiateMsg { pool_asset_configs: vec![ AssetConfig::from_denom_str("uosmo"), @@ -1098,7 +1102,7 @@ mod tests { moderator: moderator.to_string(), }; let env = mock_env(); - let info = mock_info(admin, &[]); + let info = message_info(&admin, &[]); // Instantiate the contract. instantiate(deps.as_mut(), env.clone(), info.clone(), init_msg).unwrap(); @@ -1119,18 +1123,19 @@ mod tests { } .into(), ), + msg_responses: vec![], }), + payload: Binary::new(vec![]), + gas_used: 0, }, ) .unwrap(); // join pool - let info = mock_info( - "someone", - &[ - Coin::new(1000000000, "uosmo"), - Coin::new(1000000000, "uion"), - ], + let someone = deps.api.addr_make("someone"); + let info = message_info( + &someone, + &[coin(1000000000, "uosmo"), coin(1000000000, "uion")], ); let join_pool_msg = ContractExecMsg::Transmuter(ExecMsg::JoinPool {}); execute(deps.as_mut(), env.clone(), info.clone(), join_pool_msg).unwrap(); @@ -1141,7 +1146,7 @@ mod tests { denoms: vec!["uosmo".to_string(), "uion".to_string()], }); - let info = mock_info(admin, &[]); + let info = message_info(&admin, &[]); execute( deps.as_mut(), env.clone(), @@ -1178,7 +1183,7 @@ mod tests { ) .unwrap(); - let info = mock_info(admin, &[]); + let info = message_info(&admin, &[]); for denom in ["uosmo", "uion"] { let register_limiter_msg = ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { scope: Scope::Denom(denom.to_string()), @@ -1213,39 +1218,35 @@ mod tests { let mut env = env.clone(); env.block.time = env.block.time.plus_nanos(360); - let info = mock_info( - "someone", - &[Coin::new(550, "uosmo"), Coin::new(500, "uion")], - ); + let someone = deps.api.addr_make("someone"); + let info = message_info(&someone, &[coin(550, "uosmo"), coin(500, "uion")]); let join_pool_msg = ContractExecMsg::Transmuter(ExecMsg::JoinPool {}); execute(deps.as_mut(), env.clone(), info.clone(), join_pool_msg).unwrap(); env.block.time = env.block.time.plus_nanos(3000); - let info = mock_info( - "someone", - &[Coin::new(450, "uosmo"), Coin::new(500, "uion")], - ); + let info = message_info(&someone, &[coin(450, "uosmo"), coin(500, "uion")]); let join_pool_msg = ContractExecMsg::Transmuter(ExecMsg::JoinPool {}); execute(deps.as_mut(), env.clone(), info.clone(), join_pool_msg).unwrap(); for denom in ["uosmo", "uion"] { assert_dirty_change_limiters_by_scope!( &Scope::denom(denom), - Transmuter::default().limiters, + Transmuter::new().limiters, deps.as_ref().storage ); } assert_dirty_change_limiters_by_scope!( &Scope::asset_group("group1"), - Transmuter::default().limiters, + Transmuter::new().limiters, deps.as_ref().storage ); // Add new assets // Attempt to add assets with invalid denom - let info = mock_info(admin, &[]); + let admin = deps.api.addr_make("admin"); + let info = message_info(&admin, &[]); let invalid_denoms = vec!["invalid_asset1".to_string(), "invalid_asset2".to_string()]; let add_invalid_assets_msg = ContractExecMsg::Transmuter(ExecMsg::AddNewAssets { asset_configs: invalid_denoms @@ -1281,8 +1282,9 @@ mod tests { env.block.time = env.block.time.plus_nanos(360); + let non_admin = deps.api.addr_make("non_admin"); // Attempt to add assets by non-admin - let non_admin_info = mock_info("non_admin", &[]); + let non_admin_info = message_info(&non_admin, &[]); let res = execute( deps.as_mut(), env.clone(), @@ -1303,7 +1305,7 @@ mod tests { execute(deps.as_mut(), env.clone(), info, add_assets_msg).unwrap(); let reset_at = env.block.time; - let transmuter = Transmuter::default(); + let transmuter = Transmuter::new(); // Reset change limiter states if new assets are added for denom in ["uosmo", "uion"] { @@ -1338,10 +1340,10 @@ mod tests { assert_eq!( total_pool_liquidity, vec![ - Coin::new(1000001000, "uosmo"), - Coin::new(1000001000, "uion"), - Coin::new(0, "new_asset1"), - Coin::new(0, "new_asset2"), + coin(1000001000, "uosmo"), + coin(1000001000, "uion"), + coin(0, "new_asset1"), + coin(0, "new_asset2"), ] ); } @@ -1350,19 +1352,20 @@ mod tests { fn test_corrupted_assets() { let mut deps = mock_dependencies(); + let someone = deps.api.addr_make("someone"); // make denom has non-zero total supply - deps.querier.update_balance( - "someone", + deps.querier.bank.update_balance( + &someone, vec![ - Coin::new(1, "wbtc"), - Coin::new(1, "tbtc"), - Coin::new(1, "nbtc"), - Coin::new(1, "stbtc"), + coin(1, "wbtc"), + coin(1, "tbtc"), + coin(1, "nbtc"), + coin(1, "stbtc"), ], ); - let admin = "admin"; - let moderator = "moderator"; + let admin = deps.api.addr_make("admin"); + let moderator = deps.api.addr_make("moderator"); let alloyed_subdenom = "btc"; let init_msg = InstantiateMsg { pool_asset_configs: vec![ @@ -1379,7 +1382,7 @@ mod tests { let env = mock_env(); // Instantiate the contract. - let info = mock_info(admin, &[]); + let info = message_info(&admin, &[]); instantiate(deps.as_mut(), env.clone(), info.clone(), init_msg).unwrap(); // Manually reply @@ -1396,7 +1399,10 @@ mod tests { } .into(), ), + msg_responses: vec![], }), + payload: Binary::new(vec![]), + gas_used: 0, }, ) .unwrap(); @@ -1419,7 +1425,7 @@ mod tests { }; // Mark corrupted assets by non-moderator - let info = mock_info("someone", &[]); + let info = message_info(&someone, &[]); let mark_corrupted_assets_msg = ContractExecMsg::Transmuter(ExecMsg::MarkCorruptedScopes { scopes: vec![Scope::denom("wbtc"), Scope::denom("tbtc")], }); @@ -1459,23 +1465,25 @@ mod tests { assert_eq!( total_pool_liquidity, vec![ - Coin::new(0, "wbtc"), - Coin::new(0, "tbtc"), - Coin::new(0, "nbtc"), - Coin::new(0, "stbtc"), + coin(0, "wbtc"), + coin(0, "tbtc"), + coin(0, "nbtc"), + coin(0, "stbtc"), ] ); // provide some liquidity let liquidity = vec![ - Coin::new(1_000_000_000_000, "wbtc"), - Coin::new(1_000_000_000_000, "tbtc"), - Coin::new(1_000_000_000_000, "nbtc"), - Coin::new(1_000_000_000_000, "stbtc"), + coin(1_000_000_000_000, "wbtc"), + coin(1_000_000_000_000, "tbtc"), + coin(1_000_000_000_000, "nbtc"), + coin(1_000_000_000_000, "stbtc"), ]; - deps.querier.update_balance("someone", liquidity.clone()); + deps.querier + .bank + .update_balance("someone", liquidity.clone()); - let info = mock_info("someone", &liquidity); + let info = message_info(&someone, &liquidity); let join_pool_msg = ContractExecMsg::Transmuter(ExecMsg::JoinPool {}); execute(deps.as_mut(), env.clone(), info.clone(), join_pool_msg).unwrap(); @@ -1488,7 +1496,7 @@ mod tests { limiter_params: change_limiter_params.clone(), }); - let info = mock_info(admin, &[]); + let info = message_info(&admin, &[]); execute( deps.as_mut(), env.clone(), @@ -1517,7 +1525,7 @@ mod tests { denoms: vec!["nbtc".to_string(), "stbtc".to_string()], }); - let info = mock_info(admin, &[]); + let info = message_info(&admin, &[]); execute( deps.as_mut(), env.clone(), @@ -1543,12 +1551,13 @@ mod tests { // exit pool a bit to make sure the limiters are dirty deps.querier - .update_balance("someone", vec![Coin::new(1_000, alloyed_denom.clone())]); + .bank + .update_balance(&someone, vec![coin(1_000, alloyed_denom.clone())]); let exit_pool_msg = ContractExecMsg::Transmuter(ExecMsg::ExitPool { - tokens_out: vec![Coin::new(1_000, "nbtc")], + tokens_out: vec![coin(1_000, "nbtc")], }); - let info = mock_info("someone", &[]); + let info = message_info(&someone, &[]); execute(deps.as_mut(), env.clone(), info.clone(), exit_pool_msg).unwrap(); // Mark corrupted assets by moderator @@ -1557,7 +1566,7 @@ mod tests { scopes: corrupted_scopes.clone(), }); - let info = mock_info(moderator, &[]); + let info = message_info(&moderator, &[]); let res = execute( deps.as_mut(), env.clone(), @@ -1596,55 +1605,57 @@ mod tests { assert_eq!( total_pool_liquidity, vec![ - Coin::new(1_000_000_000_000, "wbtc"), - Coin::new(1_000_000_000_000, "tbtc"), - Coin::new(999_999_999_000, "nbtc"), - Coin::new(1_000_000_000_000, "stbtc"), + coin(1_000_000_000_000, "wbtc"), + coin(1_000_000_000_000, "tbtc"), + coin(999_999_999_000, "nbtc"), + coin(1_000_000_000_000, "stbtc"), ] ); // warm up the limiters let env = increase_block_height(&env, 1); deps.querier - .update_balance("someone", vec![Coin::new(4, alloyed_denom.clone())]); + .bank + .update_balance(&someone, vec![coin(4, alloyed_denom.clone())]); let exit_pool_msg = ContractExecMsg::Transmuter(ExecMsg::ExitPool { tokens_out: vec![ - Coin::new(1, "wbtc"), - Coin::new(1, "tbtc"), - Coin::new(1, "nbtc"), - Coin::new(1, "stbtc"), + coin(1, "wbtc"), + coin(1, "tbtc"), + coin(1, "nbtc"), + coin(1, "stbtc"), ], }); - let info = mock_info("someone", &[]); + let info = message_info(&someone, &[]); execute(deps.as_mut(), env.clone(), info.clone(), exit_pool_msg).unwrap(); for denom in ["wbtc", "tbtc", "nbtc", "stbtc"] { assert_dirty_change_limiters_by_scope!( &Scope::denom(denom), - Transmuter::default().limiters, + Transmuter::new().limiters, deps.as_ref().storage ); } assert_dirty_change_limiters_by_scope!( &Scope::asset_group("btc_group1"), - Transmuter::default().limiters, + Transmuter::new().limiters, deps.as_ref().storage ); let env = increase_block_height(&env, 1); deps.querier - .update_balance("someone", vec![Coin::new(4, alloyed_denom.clone())]); + .bank + .update_balance("someone", vec![coin(4, alloyed_denom.clone())]); let exit_pool_msg = ContractExecMsg::Transmuter(ExecMsg::ExitPool { tokens_out: vec![ - Coin::new(1, "wbtc"), - Coin::new(1, "tbtc"), - Coin::new(1, "nbtc"), - Coin::new(1, "stbtc"), + coin(1, "wbtc"), + coin(1, "tbtc"), + coin(1, "nbtc"), + coin(1, "stbtc"), ], }); - let info = mock_info("someone", &[]); + let info = message_info(&someone, &[]); execute(deps.as_mut(), env.clone(), info.clone(), exit_pool_msg).unwrap(); let env = increase_block_height(&env, 1); @@ -1660,21 +1671,23 @@ mod tests { }; // join with corrupted denom should fail + let user = deps.api.addr_make("user"); let join_pool_msg = ContractExecMsg::Transmuter(ExecMsg::JoinPool {}); let err = execute( deps.as_mut(), env.clone(), - mock_info("user", &[Coin::new(1000, denom.clone())]), + message_info(&user, &[coin(1000, denom.clone())]), join_pool_msg, ) .unwrap_err(); assert_eq!(expected_err, err); + let mock_sender = deps.api.addr_make("mock_sender"); // swap exact in with corrupted denom as token in should fail let swap_msg = SudoMsg::SwapExactAmountIn { - token_in: Coin::new(1000, denom.clone()), + token_in: coin(1000, denom.clone()), swap_fee: Decimal::zero(), - sender: "mock_sender".to_string(), + sender: mock_sender.to_string(), token_out_denom: "nbtc".to_string(), token_out_min_amount: Uint128::new(500), }; @@ -1684,9 +1697,9 @@ mod tests { // swap exact in with corrupted denom as token out should be ok since it decreases the corrupted asset let swap_msg = SudoMsg::SwapExactAmountIn { - token_in: Coin::new(1000, "nbtc"), + token_in: coin(1000, "nbtc"), swap_fee: Decimal::zero(), - sender: "mock_sender".to_string(), + sender: mock_sender.to_string(), token_out_denom: denom.clone(), token_out_min_amount: Uint128::new(500), }; @@ -1695,8 +1708,8 @@ mod tests { // swap exact out with corrupted denom as token out should be ok since it decreases the corrupted asset let swap_msg = SudoMsg::SwapExactAmountOut { - sender: "mock_sender".to_string(), - token_out: Coin::new(500, denom.clone()), + sender: mock_sender.to_string(), + token_out: coin(500, denom.clone()), swap_fee: Decimal::zero(), token_in_denom: "nbtc".to_string(), token_in_max_amount: Uint128::new(1000), @@ -1706,8 +1719,8 @@ mod tests { // swap exact out with corrupted denom as token in should fail let swap_msg = SudoMsg::SwapExactAmountOut { - sender: "mock_sender".to_string(), - token_out: Coin::new(500, "nbtc"), + sender: mock_sender.to_string(), + token_out: coin(500, "nbtc"), swap_fee: Decimal::zero(), token_in_denom: denom.clone(), token_in_max_amount: Uint128::new(1000), @@ -1718,16 +1731,15 @@ mod tests { // exit with by any denom requires corrupted denom to not increase in weight // (this case increase other remaining corrupted denom weight) - deps.querier.update_balance( - "someone", - vec![Coin::new(4_000_000_000, alloyed_denom.clone())], - ); + deps.querier + .bank + .update_balance(&someone, vec![coin(4_000_000_000, alloyed_denom.clone())]); let exit_pool_msg = ContractExecMsg::Transmuter(ExecMsg::ExitPool { - tokens_out: vec![Coin::new(1_000_000_000, "stbtc")], + tokens_out: vec![coin(1_000_000_000, "stbtc")], }); - let info = mock_info("someone", &[]); + let info = message_info(&someone, &[]); // this causes all corrupted denoms to be increased in weight let err = execute(deps.as_mut(), env.clone(), info, exit_pool_msg).unwrap_err(); @@ -1738,12 +1750,12 @@ mod tests { let exit_pool_msg = ContractExecMsg::Transmuter(ExecMsg::ExitPool { tokens_out: vec![ - Coin::new(1_000_000_000, "nbtc"), - Coin::new(1_000_000_000, denom.clone()), + coin(1_000_000_000, "nbtc"), + coin(1_000_000_000, denom.clone()), ], }); - let info = mock_info("someone", &[]); + let info = message_info(&someone, &[]); // this causes other corrupted denom to be increased relatively let err = execute(deps.as_mut(), env.clone(), info, exit_pool_msg).unwrap_err(); @@ -1754,32 +1766,31 @@ mod tests { } // exit with corrupted denom requires all corrupted denom exit with the same value - deps.querier.update_balance( - "someone", - vec![Coin::new(4_000_000_000, alloyed_denom.clone())], - ); - let info = mock_info("someone", &[]); + deps.querier + .bank + .update_balance(&someone, vec![coin(4_000_000_000, alloyed_denom.clone())]); + let info = message_info(&someone, &[]); let exit_pool_msg = ContractExecMsg::Transmuter(ExecMsg::ExitPool { tokens_out: vec![ - Coin::new(2_000_000_000, "nbtc"), - Coin::new(1_000_000_000, "wbtc"), - Coin::new(1_000_000_000, "tbtc"), + coin(2_000_000_000, "nbtc"), + coin(1_000_000_000, "wbtc"), + coin(1_000_000_000, "tbtc"), ], }); execute(deps.as_mut(), env.clone(), info, exit_pool_msg).unwrap(); // force redeem corrupted assets - deps.querier.update_balance( - "someone", - vec![Coin::new(1_000_000_000_000, alloyed_denom.clone())], + deps.querier.bank.update_balance( + &someone, + vec![coin(1_000_000_000_000, alloyed_denom.clone())], ); let all_nbtc = total_liquidity_of("nbtc", &deps.storage); let force_redeem_corrupted_assets_msg = ContractExecMsg::Transmuter(ExecMsg::ExitPool { tokens_out: vec![all_nbtc], }); - let info = mock_info("someone", &[]); + let info = message_info(&someone, &[]); let err = execute( deps.as_mut(), env.clone(), @@ -1800,12 +1811,12 @@ mod tests { tokens_out: vec![all_wbtc], }); - deps.querier.update_balance( - "someone", - vec![Coin::new(1_000_000_000_000, alloyed_denom.clone())], + deps.querier.bank.update_balance( + &someone, + vec![coin(1_000_000_000_000, alloyed_denom.clone())], ); - let info = mock_info("someone", &[]); + let info = message_info(&someone, &[]); execute( deps.as_mut(), env.clone(), @@ -1830,14 +1841,14 @@ mod tests { assert_eq!( total_pool_liquidity, vec![ - Coin::new(998999998498, "tbtc"), - Coin::new(998000001998, "nbtc"), - Coin::new(999999999998, "stbtc"), + coin(998999998498, "tbtc"), + coin(998000001998, "nbtc"), + coin(999999999998, "stbtc"), ] ); assert_eq!( - Transmuter::default() + Transmuter::new() .limiters .list_limiters_by_scope(&deps.storage, &Scope::denom("wbtc")) .unwrap(), @@ -1848,7 +1859,7 @@ mod tests { assert_reset_change_limiters_by_scope!( &Scope::denom(denom), env.block.time, - Transmuter::default(), + Transmuter::new(), deps.as_ref().storage ); } @@ -1856,7 +1867,7 @@ mod tests { assert_reset_change_limiters_by_scope!( &Scope::asset_group("btc_group1"), env.block.time, - Transmuter::default(), + Transmuter::new(), deps.as_ref().storage ); @@ -1866,7 +1877,7 @@ mod tests { scopes: vec![Scope::denom("nbtc")], }); - let info = mock_info(moderator, &[]); + let info = message_info(&moderator, &[]); let err = execute( deps.as_mut(), env.clone(), @@ -1888,7 +1899,7 @@ mod tests { scopes: vec![Scope::denom("tbtc")], }); - let info = mock_info("someone", &[]); + let info = message_info(&someone, &[]); let err = execute( deps.as_mut(), env.clone(), @@ -1905,7 +1916,7 @@ mod tests { scopes: vec![Scope::denom("tbtc")], }); - let info = mock_info(moderator, &[]); + let info = message_info(&moderator, &[]); execute( deps.as_mut(), env.clone(), @@ -1942,15 +1953,15 @@ mod tests { assert_eq!( total_pool_liquidity, vec![ - Coin::new(998999998498, "tbtc"), - Coin::new(998000001998, "nbtc"), - Coin::new(999999999998, "stbtc"), + coin(998999998498, "tbtc"), + coin(998000001998, "nbtc"), + coin(999999999998, "stbtc"), ] ); // still has all the limiters assert_eq!( - Transmuter::default() + Transmuter::new() .limiters .list_limiters_by_scope(&deps.storage, &Scope::denom("tbtc")) .unwrap() @@ -1962,18 +1973,23 @@ mod tests { #[test] fn test_corrupted_asset_group() { let mut deps = mock_dependencies(); + let admin = deps.api.addr_make("admin"); + let moderator = deps.api.addr_make("moderator"); + let user = deps.api.addr_make("user"); + let moderator = deps.api.addr_make("moderator"); - deps.querier.update_balance( - "admin", + let info = message_info(&admin, &[]); + + deps.querier.bank.update_balance( + &admin, vec![ - Coin::new(1_000_000_000_000, "tbtc"), - Coin::new(1_000_000_000_000, "nbtc"), - Coin::new(1_000_000_000_000, "stbtc"), + coin(1_000_000_000_000, "tbtc"), + coin(1_000_000_000_000, "nbtc"), + coin(1_000_000_000_000, "stbtc"), ], ); let env = mock_env(); - let info = mock_info("admin", &[]); // Initialize contract with asset group let init_msg = InstantiateMsg { @@ -1984,8 +2000,8 @@ mod tests { ], alloyed_asset_subdenom: "btc".to_string(), alloyed_asset_normalization_factor: Uint128::one(), - admin: Some("admin".to_string()), - moderator: "moderator".to_string(), + admin: Some(admin.to_string()), + moderator: moderator.to_string(), }; instantiate(deps.as_mut(), env.clone(), info.clone(), init_msg).unwrap(); @@ -2004,7 +2020,10 @@ mod tests { } .into(), ), + msg_responses: vec![], }), + payload: Binary::new(vec![]), + gas_used: 0, }, ) .unwrap(); @@ -2016,10 +2035,9 @@ mod tests { .unwrap() .value; - deps.querier.update_balance( - "user", - vec![Coin::new(3_000_000_000_000, alloyed_denom.clone())], - ); + deps.querier + .bank + .update_balance(&user, vec![coin(3_000_000_000_000, alloyed_denom.clone())]); // Create asset group let create_group_msg = ContractExecMsg::Transmuter(ExecMsg::CreateAssetGroup { @@ -2029,7 +2047,7 @@ mod tests { execute(deps.as_mut(), env.clone(), info.clone(), create_group_msg).unwrap(); // Set change limiter for btc group - let info = mock_info("admin", &[]); + let info = message_info(&admin, &[]); let set_limiter_msg = ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { scope: Scope::asset_group("group1"), label: "big_change_limiter".to_string(), @@ -2062,12 +2080,12 @@ mod tests { execute( deps.as_mut(), env.clone(), - mock_info( - "user", + message_info( + &user, &[ - Coin::new(1_000_000_000_000, "tbtc"), - Coin::new(1_000_000_000_000, "nbtc"), - Coin::new(1_000_000_000_000, "stbtc"), + coin(1_000_000_000_000, "tbtc"), + coin(1_000_000_000_000, "nbtc"), + coin(1_000_000_000_000, "stbtc"), ], ), add_liquidity_msg, @@ -2077,12 +2095,12 @@ mod tests { // Assert dirty change limiters for the asset group assert_dirty_change_limiters_by_scope!( &Scope::asset_group("group1"), - &Transmuter::default().limiters, + &Transmuter::new().limiters, &deps.storage ); // Mark asset group as corrupted - let info = mock_info("moderator", &[]); + let info = message_info(&moderator, &[]); let mark_corrupted_msg = ContractExecMsg::Transmuter(ExecMsg::MarkCorruptedScopes { scopes: vec![Scope::asset_group("group1")], }); @@ -2102,11 +2120,11 @@ mod tests { // Exit pool with all corrupted assets let env = increase_block_height(&env, 1); - let info = mock_info("user", &[]); + let info = message_info(&user, &[]); let exit_pool_msg = ContractExecMsg::Transmuter(ExecMsg::ExitPool { tokens_out: vec![ - Coin::new(1_000_000_000_000, "tbtc"), - Coin::new(1_000_000_000_000, "nbtc"), + coin(1_000_000_000_000, "tbtc"), + coin(1_000_000_000_000, "nbtc"), ], }); execute(deps.as_mut(), env.clone(), info.clone(), exit_pool_msg).unwrap(); @@ -2115,7 +2133,7 @@ mod tests { assert_reset_change_limiters_by_scope!( &Scope::asset_group("group1"), env.block.time, - Transmuter::default(), + Transmuter::new(), &deps.storage ); @@ -2140,13 +2158,10 @@ mod tests { total_pool_liquidity, } = from_json(res).unwrap(); - assert_eq!( - total_pool_liquidity, - vec![Coin::new(1_000_000_000_000, "stbtc")] - ); + assert_eq!(total_pool_liquidity, vec![coin(1_000_000_000_000, "stbtc")]); // Assert that only one limiter remains for stbtc - let limiters = Transmuter::default() + let limiters = Transmuter::new() .limiters .list_limiters(&deps.storage) .unwrap() @@ -2162,7 +2177,7 @@ mod tests { assert_reset_change_limiters_by_scope!( &Scope::denom("stbtc"), env.block.time, - Transmuter::default(), + Transmuter::new(), &deps.storage ); } @@ -2180,7 +2195,7 @@ mod tests { } fn total_liquidity_of(denom: &str, storage: &dyn Storage) -> Coin { - Transmuter::default() + Transmuter::new() .pool .load(storage) .unwrap() @@ -2195,12 +2210,16 @@ mod tests { fn test_set_active_status() { let mut deps = mock_dependencies(); + let someone = deps.api.addr_make("someone"); + let user = deps.api.addr_make("user"); + let admin = deps.api.addr_make("admin"); + let moderator = deps.api.addr_make("moderator"); + let non_moderator = deps.api.addr_make("non_moderator"); // make denom has non-zero total supply deps.querier - .update_balance("someone", vec![Coin::new(1, "uosmo"), Coin::new(1, "uion")]); + .bank + .update_balance(&someone, vec![coin(1, "uosmo"), coin(1, "uion")]); - let admin = "admin"; - let moderator = "moderator"; let init_msg = InstantiateMsg { pool_asset_configs: vec![ AssetConfig::from_denom_str("uosmo"), @@ -2212,7 +2231,7 @@ mod tests { moderator: moderator.to_string(), }; let env = mock_env(); - let info = mock_info(admin, &[]); + let info = message_info(&admin, &[]); // Instantiate the contract. instantiate(deps.as_mut(), env.clone(), info, init_msg).unwrap(); @@ -2220,7 +2239,7 @@ mod tests { // Manually set alloyed denom let alloyed_denom = "uosmo".to_string(); - let transmuter = Transmuter::default(); + let transmuter = Transmuter::new(); transmuter .alloyed_asset .set_alloyed_denom(&mut deps.storage, &alloyed_denom) @@ -2237,9 +2256,16 @@ mod tests { assert!(active_status.is_active); // Attempt to set the active status by a non-admin user. - let non_admin_info = mock_info("non_moderator", &[]); - let non_admin_msg = ContractExecMsg::Transmuter(ExecMsg::SetActiveStatus { active: false }); - let err = execute(deps.as_mut(), env.clone(), non_admin_info, non_admin_msg).unwrap_err(); + let non_moderator_info = message_info(&non_moderator, &[]); + let non_moderator_msg = + ContractExecMsg::Transmuter(ExecMsg::SetActiveStatus { active: false }); + let err = execute( + deps.as_mut(), + env.clone(), + non_moderator_info, + non_moderator_msg, + ) + .unwrap_err(); assert_eq!(err, ContractError::Unauthorized {}); @@ -2248,7 +2274,7 @@ mod tests { execute( deps.as_mut(), env.clone(), - mock_info(moderator, &[]), + message_info(&moderator, &[]), msg.clone(), ) .unwrap(); @@ -2264,7 +2290,13 @@ mod tests { assert!(!active_status.is_active); // try to set the active status to false again - let err = execute(deps.as_mut(), env.clone(), mock_info(moderator, &[]), msg).unwrap_err(); + let err = execute( + deps.as_mut(), + env.clone(), + message_info(&moderator, &[]), + msg, + ) + .unwrap_err(); assert_eq!(err, ContractError::UnchangedActiveStatus { status: false }); // Test that JoinPool is blocked when active status is false @@ -2272,7 +2304,7 @@ mod tests { let err = execute( deps.as_mut(), env.clone(), - mock_info("user", &[Coin::new(1000, "uion"), Coin::new(1000, "uosmo")]), + message_info(&user, &[coin(1000, "uion"), coin(1000, "uosmo")]), join_pool_msg, ) .unwrap_err(); @@ -2280,7 +2312,7 @@ mod tests { // Test that SwapExactAmountIn is blocked when active status is false let swap_exact_amount_in_msg = SudoMsg::SwapExactAmountIn { - token_in: Coin::new(1000, "uion"), + token_in: coin(1000, "uion"), swap_fee: Decimal::zero(), sender: "mock_sender".to_string(), token_out_denom: "uosmo".to_string(), @@ -2292,7 +2324,7 @@ mod tests { // Test that SwapExactAmountOut is blocked when active status is false let swap_exact_amount_out_msg = SudoMsg::SwapExactAmountOut { sender: "mock_sender".to_string(), - token_out: Coin::new(500, "uosmo"), + token_out: coin(500, "uosmo"), swap_fee: Decimal::zero(), token_in_denom: "uion".to_string(), token_in_max_amount: Uint128::new(1000), @@ -2302,12 +2334,12 @@ mod tests { // Test that ExitPool is blocked when active status is false let exit_pool_msg = ContractExecMsg::Transmuter(ExecMsg::ExitPool { - tokens_out: vec![Coin::new(1000, "uion"), Coin::new(1000, "uosmo")], + tokens_out: vec![coin(1000, "uion"), coin(1000, "uosmo")], }); let err = execute( deps.as_mut(), env.clone(), - mock_info("user", &[Coin::new(1000, "uion"), Coin::new(1000, "uosmo")]), + message_info(&user, &[coin(1000, "uion"), coin(1000, "uosmo")]), exit_pool_msg, ) .unwrap_err(); @@ -2315,7 +2347,13 @@ mod tests { // Set the active status back to true let msg = ContractExecMsg::Transmuter(ExecMsg::SetActiveStatus { active: true }); - execute(deps.as_mut(), env.clone(), mock_info(moderator, &[]), msg).unwrap(); + execute( + deps.as_mut(), + env.clone(), + message_info(&moderator, &[]), + msg, + ) + .unwrap(); // Check the active status again. let res = query( @@ -2332,26 +2370,29 @@ mod tests { let res = execute( deps.as_mut(), env.clone(), - mock_info("user", &[Coin::new(1000, "uion"), Coin::new(1000, "uosmo")]), + message_info(&user, &[coin(1000, "uion"), coin(1000, "uosmo")]), join_pool_msg, ); assert!(res.is_ok()); + let mock_sender = deps.api.addr_make("mock_sender"); + // Test that SwapExactAmountIn is active when active status is true let swap_exact_amount_in_msg = SudoMsg::SwapExactAmountIn { - token_in: Coin::new(100, "uion"), + token_in: coin(100, "uion"), swap_fee: Decimal::zero(), - sender: "mock_sender".to_string(), + sender: mock_sender.to_string(), token_out_denom: "uosmo".to_string(), token_out_min_amount: Uint128::new(100), }; let res = sudo(deps.as_mut(), env.clone(), swap_exact_amount_in_msg); assert!(res.is_ok()); + let mock_sender = deps.api.addr_make("mock_sender"); // Test that SwapExactAmountOut is active when active status is true let swap_exact_amount_out_msg = SudoMsg::SwapExactAmountOut { - sender: "mock_sender".to_string(), - token_out: Coin::new(100, "uosmo"), + sender: mock_sender.into_string(), + token_out: coin(100, "uosmo"), swap_fee: Decimal::zero(), token_in_denom: "uion".to_string(), token_in_max_amount: Uint128::new(100), @@ -2404,13 +2445,14 @@ mod tests { // make denom has non-zero total supply deps.querier - .update_balance("someone", vec![Coin::new(1, "uosmo"), Coin::new(1, "uion")]); - - let admin = "admin"; - let moderator = "moderator"; - let canceling_candidate = "canceling_candidate"; - let rejecting_candidate = "rejecting_candidate"; - let candidate = "candidate"; + .bank + .update_balance("someone", vec![coin(1, "uosmo"), coin(1, "uion")]); + + let admin = deps.api.addr_make("admin"); + let moderator = deps.api.addr_make("moderator"); + let canceling_candidate = deps.api.addr_make("canceling_candidate"); + let rejecting_candidate = deps.api.addr_make("rejecting_candidate"); + let candidate = deps.api.addr_make("candidate"); let init_msg = InstantiateMsg { pool_asset_configs: vec![ AssetConfig::from_denom_str("uosmo"), @@ -2422,7 +2464,7 @@ mod tests { moderator: moderator.to_string(), }; let env = mock_env(); - let info = mock_info(admin, &[]); + let info = message_info(&admin, &[]); // Instantiate the contract. instantiate(deps.as_mut(), env.clone(), info.clone(), init_msg).unwrap(); @@ -2443,7 +2485,7 @@ mod tests { let admin_candidate: GetAdminCandidateResponse = from_json(res).unwrap(); assert_eq!( admin_candidate.admin_candidate.unwrap().as_str(), - canceling_candidate + canceling_candidate.as_str() ); // Cancel admin rights transfer @@ -2453,7 +2495,7 @@ mod tests { execute( deps.as_mut(), env.clone(), - mock_info(admin, &[]), + message_info(&admin, &[]), cancel_admin_transfer_msg, ) .unwrap(); @@ -2484,7 +2526,7 @@ mod tests { let admin_candidate: GetAdminCandidateResponse = from_json(res).unwrap(); assert_eq!( admin_candidate.admin_candidate.unwrap().as_str(), - rejecting_candidate + rejecting_candidate.as_str() ); // Reject admin rights transfer @@ -2494,7 +2536,7 @@ mod tests { execute( deps.as_mut(), env.clone(), - mock_info(rejecting_candidate, &[]), + message_info(&rejecting_candidate, &[]), reject_admin_transfer_msg, ) .unwrap(); @@ -2523,14 +2565,17 @@ mod tests { ) .unwrap(); let admin_candidate: GetAdminCandidateResponse = from_json(res).unwrap(); - assert_eq!(admin_candidate.admin_candidate.unwrap().as_str(), candidate); + assert_eq!( + admin_candidate.admin_candidate.unwrap().as_str(), + candidate.as_str() + ); // Claim admin rights by the candidate let claim_admin_msg = ContractExecMsg::Transmuter(ExecMsg::ClaimAdmin {}); execute( deps.as_mut(), env.clone(), - mock_info(candidate, &[]), + message_info(&candidate, &[]), claim_admin_msg, ) .unwrap(); @@ -2543,19 +2588,20 @@ mod tests { ) .unwrap(); let admin: GetAdminResponse = from_json(res).unwrap(); - assert_eq!(admin.admin.as_str(), candidate); + assert_eq!(admin.admin.as_str(), candidate.as_str()); } #[test] fn test_assign_and_remove_moderator() { - let admin = "admin"; - let moderator = "moderator"; - let mut deps = mock_dependencies(); + let admin = deps.api.addr_make("admin"); + let moderator = deps.api.addr_make("moderator"); + let someone = deps.api.addr_make("someone"); // make denom has non-zero total supply deps.querier - .update_balance("someone", vec![Coin::new(1, "uosmo"), Coin::new(1, "uion")]); + .bank + .update_balance(someone, vec![coin(1, "uosmo"), coin(1, "uion")]); // Instantiate the contract. let init_msg = InstantiateMsg { @@ -2568,7 +2614,13 @@ mod tests { alloyed_asset_normalization_factor: Uint128::one(), moderator: moderator.to_string(), }; - instantiate(deps.as_mut(), mock_env(), mock_info(admin, &[]), init_msg).unwrap(); + instantiate( + deps.as_mut(), + mock_env(), + message_info(&admin, &[]), + init_msg, + ) + .unwrap(); // Check the current moderator let res = query( @@ -2578,15 +2630,19 @@ mod tests { ) .unwrap(); let moderator_response: GetModeratorResponse = from_json(res).unwrap(); - assert_eq!(moderator_response.moderator, moderator); + assert_eq!( + moderator_response.moderator.into_string(), + moderator.into_string() + ); - let new_moderator = "new_moderator"; + let new_moderator = deps.api.addr_make("new_moderator"); + let non_admin = deps.api.addr_make("non_admin"); // Try to assign new moderator by non admin let err = execute( deps.as_mut(), mock_env(), - mock_info("non_admin", &[]), + message_info(&non_admin, &[]), ContractExecMsg::Transmuter(ExecMsg::AssignModerator { address: new_moderator.to_string(), }), @@ -2599,7 +2655,7 @@ mod tests { execute( deps.as_mut(), mock_env(), - mock_info(admin, &[]), + message_info(&admin, &[]), ContractExecMsg::Transmuter(ExecMsg::AssignModerator { address: new_moderator.to_string(), }), @@ -2614,7 +2670,10 @@ mod tests { ) .unwrap(); let moderator_response: GetModeratorResponse = from_json(res).unwrap(); - assert_eq!(moderator_response.moderator, new_moderator); + assert_eq!( + moderator_response.moderator.to_string(), + new_moderator.to_string() + ); } #[test] @@ -2624,28 +2683,36 @@ mod tests { // make denom has non-zero total supply deps.querier - .update_balance("someone", vec![Coin::new(1, "uosmo"), Coin::new(1, "uion")]); + .bank + .update_balance("someone", vec![coin(1, "uosmo"), coin(1, "uion")]); - let admin = "admin"; - let user = "user"; + let admin = deps.api.addr_make("admin"); + let user = deps.api.addr_make("user"); + let moderator = deps.api.addr_make("moderator"); let init_msg = InstantiateMsg { pool_asset_configs: vec![ AssetConfig::from_denom_str("uosmo"), AssetConfig::from_denom_str("uion"), ], admin: Some(admin.to_string()), - moderator: "moderator".to_string(), + moderator: moderator.to_string(), alloyed_asset_subdenom: "usomoion".to_string(), alloyed_asset_normalization_factor: Uint128::one(), }; - instantiate(deps.as_mut(), mock_env(), mock_info(admin, &[]), init_msg).unwrap(); + instantiate( + deps.as_mut(), + mock_env(), + message_info(&admin, &[]), + init_msg, + ) + .unwrap(); // normal user can't register limiter let err = execute( deps.as_mut(), mock_env(), - mock_info(user, &[]), + message_info(&user, &[]), ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { scope: Scope::Denom("uosmo".to_string()), label: "1h".to_string(), @@ -2665,7 +2732,7 @@ mod tests { let err = execute( deps.as_mut(), mock_env(), - mock_info(user, &[]), + message_info(&user, &[]), ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { scope: Scope::Denom("uosmo".to_string()), label: "1h".to_string(), @@ -2686,7 +2753,7 @@ mod tests { let res = execute( deps.as_mut(), mock_env(), - mock_info(admin, &[]), + message_info(&admin, &[]), ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { scope: Scope::Denom("uosmo".to_string()), label: "1h".to_string(), @@ -2714,7 +2781,7 @@ mod tests { let err = execute( deps.as_mut(), mock_env(), - mock_info(admin, &[]), + message_info(&admin, &[]), ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { scope: Scope::Denom("invalid_denom".to_string()), label: "1h".to_string(), @@ -2755,7 +2822,7 @@ mod tests { let res = execute( deps.as_mut(), mock_env(), - mock_info(admin, &[]), + message_info(&admin, &[]), ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { scope: Scope::Denom("uosmo".to_string()), label: "1w".to_string(), @@ -2806,7 +2873,7 @@ mod tests { let res = execute( deps.as_mut(), mock_env(), - mock_info(admin, &[]), + message_info(&admin, &[]), ContractExecMsg::Transmuter(ExecMsg::RegisterLimiter { scope: Scope::Denom("uosmo".to_string()), label: "static".to_string(), @@ -2831,7 +2898,7 @@ mod tests { let err = execute( deps.as_mut(), mock_env(), - mock_info(user, &[]), + message_info(&user, &[]), ContractExecMsg::Transmuter(ExecMsg::DeregisterLimiter { scope: Scope::Denom("uosmo".to_string()), label: "1h".to_string(), @@ -2845,7 +2912,7 @@ mod tests { let res = execute( deps.as_mut(), mock_env(), - mock_info(admin, &[]), + message_info(&admin, &[]), ContractExecMsg::Transmuter(ExecMsg::DeregisterLimiter { scope: Scope::Denom("uosmo".to_string()), label: "1h".to_string(), @@ -2886,7 +2953,7 @@ mod tests { let err = execute( deps.as_mut(), mock_env(), - mock_info(user, &[]), + message_info(&user, &[]), ContractExecMsg::Transmuter(ExecMsg::SetChangeLimiterBoundaryOffset { scope: Scope::Denom("uosmo".to_string()), label: "1w".to_string(), @@ -2901,7 +2968,7 @@ mod tests { let err = execute( deps.as_mut(), mock_env(), - mock_info(admin, &[]), + message_info(&admin, &[]), ContractExecMsg::Transmuter(ExecMsg::SetChangeLimiterBoundaryOffset { scope: Scope::Denom("uosmo".to_string()), label: "1h".to_string(), @@ -2922,7 +2989,7 @@ mod tests { let res = execute( deps.as_mut(), mock_env(), - mock_info(admin, &[]), + message_info(&admin, &[]), ContractExecMsg::Transmuter(ExecMsg::SetChangeLimiterBoundaryOffset { scope: Scope::Denom("uosmo".to_string()), label: "1w".to_string(), @@ -2965,7 +3032,7 @@ mod tests { let err = execute( deps.as_mut(), mock_env(), - mock_info(user, &[]), + message_info(&user, &[]), ContractExecMsg::Transmuter(ExecMsg::SetStaticLimiterUpperLimit { scope: Scope::Denom("uosmo".to_string()), label: "static".to_string(), @@ -2980,7 +3047,7 @@ mod tests { let err = execute( deps.as_mut(), mock_env(), - mock_info(admin, &[]), + message_info(&admin, &[]), ContractExecMsg::Transmuter(ExecMsg::SetStaticLimiterUpperLimit { scope: Scope::Denom("uosmo".to_string()), label: "1h".to_string(), @@ -3001,7 +3068,7 @@ mod tests { let err = execute( deps.as_mut(), mock_env(), - mock_info(admin, &[]), + message_info(&admin, &[]), ContractExecMsg::Transmuter(ExecMsg::SetStaticLimiterUpperLimit { scope: Scope::denom("uosmo"), label: "1w".to_string(), @@ -3022,7 +3089,7 @@ mod tests { let res = execute( deps.as_mut(), mock_env(), - mock_info(admin, &[]), + message_info(&admin, &[]), ContractExecMsg::Transmuter(ExecMsg::SetStaticLimiterUpperLimit { scope: Scope::Denom("uosmo".to_string()), label: "static".to_string(), @@ -3068,10 +3135,12 @@ mod tests { // make denom has non-zero total supply deps.querier - .update_balance("someone", vec![Coin::new(1, "uosmo"), Coin::new(1, "uion")]); + .bank + .update_balance("someone", vec![coin(1, "uosmo"), coin(1, "uion")]); - let admin = "admin"; - let non_admin = "non_admin"; + let admin = deps.api.addr_make("admin"); + let non_admin = deps.api.addr_make("non_admin"); + let moderator = deps.api.addr_make("moderator"); let init_msg = InstantiateMsg { pool_asset_configs: vec![ AssetConfig::from_denom_str("uosmo"), @@ -3079,11 +3148,11 @@ mod tests { ], alloyed_asset_subdenom: "uosmouion".to_string(), admin: Some(admin.to_string()), - moderator: "moderator".to_string(), + moderator: moderator.to_string(), alloyed_asset_normalization_factor: Uint128::one(), }; let env = mock_env(); - let info = mock_info(admin, &[]); + let info = message_info(&admin, &[]); // Instantiate the contract. instantiate(deps.as_mut(), env.clone(), info.clone(), init_msg).unwrap(); @@ -3100,7 +3169,7 @@ mod tests { }; // Attempt to set alloyed denom metadata by a non-admin user. - let non_admin_info = mock_info(non_admin, &[]); + let non_admin_info = message_info(&non_admin, &[]); let non_admin_msg = ContractExecMsg::Transmuter(ExecMsg::SetAlloyedDenomMetadata { metadata: metadata.clone(), }); @@ -3134,10 +3203,12 @@ mod tests { // make denom has non-zero total supply deps.querier - .update_balance("someone", vec![Coin::new(1, "uosmo"), Coin::new(1, "uion")]); + .bank + .update_balance("someone", vec![coin(1, "uosmo"), coin(1, "uion")]); - let admin = "admin"; - let user = "user"; + let admin = deps.api.addr_make("admin"); + let user = deps.api.addr_make("user"); + let moderator = deps.api.addr_make("moderator"); let init_msg = InstantiateMsg { pool_asset_configs: vec![ AssetConfig::from_denom_str("uosmo"), @@ -3146,10 +3217,10 @@ mod tests { admin: Some(admin.to_string()), alloyed_asset_subdenom: "usomoion".to_string(), alloyed_asset_normalization_factor: Uint128::one(), - moderator: "moderator".to_string(), + moderator: moderator.to_string(), }; let env = mock_env(); - let info = mock_info(admin, &[]); + let info = message_info(&admin, &[]); // Instantiate the contract. instantiate(deps.as_mut(), env.clone(), info, init_msg).unwrap(); @@ -3170,21 +3241,24 @@ mod tests { } .into(), ), + msg_responses: vec![], }), + payload: Binary::new(vec![]), + gas_used: 0, }, ) .unwrap(); // join pool with amount 0 coin should error let join_pool_msg = ContractExecMsg::Transmuter(ExecMsg::JoinPool {}); - let info = mock_info(user, &[Coin::new(1000, "uion"), Coin::new(0, "uosmo")]); + let info = message_info(&user, &[coin(1000, "uion"), coin(0, "uosmo")]); let err = execute(deps.as_mut(), env.clone(), info, join_pool_msg).unwrap_err(); assert_eq!(err, ContractError::ZeroValueOperation {}); // join pool properly works let join_pool_msg = ContractExecMsg::Transmuter(ExecMsg::JoinPool {}); - let info = mock_info(user, &[Coin::new(1000, "uion"), Coin::new(1000, "uosmo")]); + let info = message_info(&user, &[coin(1000, "uion"), coin(1000, "uosmo")]); execute(deps.as_mut(), env.clone(), info, join_pool_msg).unwrap(); // Check pool asset @@ -3201,20 +3275,22 @@ mod tests { .unwrap(); assert_eq!( total_pool_liquidity, - vec![Coin::new(1000, "uosmo"), Coin::new(1000, "uion")] + vec![coin(1000, "uosmo"), coin(1000, "uion")] ); } #[test] fn test_exit_pool() { let mut deps = mock_dependencies(); - + let someone = deps.api.addr_make("someone"); + let admin = deps.api.addr_make("admin"); + let user = deps.api.addr_make("user"); + let moderator = deps.api.addr_make("moderator"); // make denom has non-zero total supply deps.querier - .update_balance("someone", vec![Coin::new(1, "uosmo"), Coin::new(1, "uion")]); + .bank + .update_balance(someone, vec![coin(1, "uosmo"), coin(1, "uion")]); - let admin = "admin"; - let user = "user"; let init_msg = InstantiateMsg { pool_asset_configs: vec![ AssetConfig::from_denom_str("uosmo"), @@ -3223,10 +3299,10 @@ mod tests { admin: Some(admin.to_string()), alloyed_asset_subdenom: "usomoion".to_string(), alloyed_asset_normalization_factor: Uint128::one(), - moderator: "moderator".to_string(), + moderator: moderator.to_string(), }; let env = mock_env(); - let info = mock_info(admin, &[]); + let info = message_info(&admin, &[]); // Instantiate the contract. instantiate(deps.as_mut(), env.clone(), info, init_msg).unwrap(); @@ -3247,24 +3323,27 @@ mod tests { } .into(), ), + msg_responses: vec![], }), + payload: Binary::new(vec![]), + gas_used: 0, }, ) .unwrap(); // join pool by others for sufficient amount let join_pool_msg = ContractExecMsg::Transmuter(ExecMsg::JoinPool {}); - let info = mock_info(admin, &[Coin::new(1000, "uion"), Coin::new(1000, "uosmo")]); + let info = message_info(&admin, &[coin(1000, "uion"), coin(1000, "uosmo")]); execute(deps.as_mut(), env.clone(), info, join_pool_msg).unwrap(); // User tries to exit pool let exit_pool_msg = ContractExecMsg::Transmuter(ExecMsg::ExitPool { - tokens_out: vec![Coin::new(1000, "uion"), Coin::new(1000, "uosmo")], + tokens_out: vec![coin(1000, "uion"), coin(1000, "uosmo")], }); let err = execute( deps.as_mut(), env.clone(), - mock_info(user, &[]), + message_info(&user, &[]), exit_pool_msg, ) .unwrap_err(); @@ -3281,22 +3360,23 @@ mod tests { let res = execute( deps.as_mut(), env.clone(), - mock_info(user, &[Coin::new(1000, "uion"), Coin::new(1000, "uosmo")]), + message_info(&user, &[coin(1000, "uion"), coin(1000, "uosmo")]), join_pool_msg, ); assert!(res.is_ok()); deps.querier - .update_balance(user, vec![Coin::new(2000, alloyed_denom)]); + .bank + .update_balance(&user, vec![coin(2000, alloyed_denom)]); // User tries to exit pool with zero amount let exit_pool_msg = ContractExecMsg::Transmuter(ExecMsg::ExitPool { - tokens_out: vec![Coin::new(0, "uion"), Coin::new(1, "uosmo")], + tokens_out: vec![coin(0, "uion"), coin(1, "uosmo")], }); let err = execute( deps.as_mut(), env.clone(), - mock_info(user, &[]), + message_info(&user, &[]), exit_pool_msg, ) .unwrap_err(); @@ -3304,12 +3384,12 @@ mod tests { // User tries to exit pool again let exit_pool_msg = ContractExecMsg::Transmuter(ExecMsg::ExitPool { - tokens_out: vec![Coin::new(1000, "uion"), Coin::new(1000, "uosmo")], + tokens_out: vec![coin(1000, "uion"), coin(1000, "uosmo")], }); let res = execute( deps.as_mut(), env.clone(), - mock_info(user, &[]), + message_info(&user, &[]), exit_pool_msg, ) .unwrap(); @@ -3318,12 +3398,12 @@ mod tests { .add_attribute("method", "exit_pool") .add_message(MsgBurn { sender: env.contract.address.to_string(), - amount: Some(Coin::new(2000u128, alloyed_denom).into()), + amount: Some(coin(2000u128, alloyed_denom).into()), burn_from_address: user.to_string(), }) .add_message(BankMsg::Send { to_address: user.to_string(), - amount: vec![Coin::new(1000, "uion"), Coin::new(1000, "uosmo")], + amount: vec![coin(1000, "uion"), coin(1000, "uosmo")], }); assert_eq!(res, expected); @@ -3332,14 +3412,17 @@ mod tests { #[test] fn test_shares_and_liquidity() { let mut deps = mock_dependencies(); + let someone = deps.api.addr_make("someone"); + let moderator = deps.api.addr_make("moderator"); // make denom has non-zero total supply deps.querier - .update_balance("someone", vec![Coin::new(1, "uosmo"), Coin::new(1, "uion")]); + .bank + .update_balance(&someone, vec![coin(1, "uosmo"), coin(1, "uion")]); - let admin = "admin"; - let user_1 = "user_1"; - let user_2 = "user_2"; + let admin = deps.api.addr_make("admin"); + let user_1 = deps.api.addr_make("user_1"); + let user_2 = deps.api.addr_make("user_2"); let init_msg = InstantiateMsg { pool_asset_configs: vec![ AssetConfig::from_denom_str("uosmo"), @@ -3348,10 +3431,10 @@ mod tests { admin: Some(admin.to_string()), alloyed_asset_subdenom: "usomoion".to_string(), alloyed_asset_normalization_factor: Uint128::one(), - moderator: "moderator".to_string(), + moderator: moderator.to_string(), }; let env = mock_env(); - let info = mock_info(admin, &[]); + let info = message_info(&admin, &[]); // Instantiate the contract. instantiate(deps.as_mut(), env.clone(), info, init_msg).unwrap(); @@ -3372,7 +3455,10 @@ mod tests { } .into(), ), + msg_responses: vec![], }), + payload: Binary::new(vec![]), + gas_used: 0, }, ) .unwrap(); @@ -3382,14 +3468,15 @@ mod tests { execute( deps.as_mut(), env.clone(), - mock_info(user_1, &[Coin::new(1000, "uion"), Coin::new(1000, "uosmo")]), + message_info(&user_1, &[coin(1000, "uion"), coin(1000, "uosmo")]), join_pool_msg, ) .unwrap(); // Update alloyed asset denom balance for user deps.querier - .update_balance(user_1, vec![Coin::new(2000, "usomoion")]); + .bank + .update_balance(&user_1, vec![coin(2000, "usomoion")]); // Query the shares of the user let res = query( @@ -3423,7 +3510,7 @@ mod tests { let total_pool_liquidity: GetTotalPoolLiquidityResponse = from_json(res).unwrap(); assert_eq!( total_pool_liquidity.total_pool_liquidity, - vec![Coin::new(1000, "uosmo"), Coin::new(1000, "uion")] + vec![coin(1000, "uosmo"), coin(1000, "uion")] ); // Join pool @@ -3431,14 +3518,15 @@ mod tests { execute( deps.as_mut(), env.clone(), - mock_info(user_2, &[Coin::new(1000, "uion")]), + message_info(&user_2, &[coin(1000, "uion")]), join_pool_msg, ) .unwrap(); // Update balance for user 2 deps.querier - .update_balance(user_2, vec![Coin::new(1000, "usomoion")]); + .bank + .update_balance(user_2, vec![coin(1000, "usomoion")]); // Query the total shares let res = query( @@ -3464,7 +3552,7 @@ mod tests { assert_eq!( total_pool_liquidity.total_pool_liquidity, - vec![Coin::new(1000, "uosmo"), Coin::new(2000, "uion")] + vec![coin(1000, "uosmo"), coin(2000, "uion")] ); } @@ -3474,9 +3562,11 @@ mod tests { // make denom has non-zero total supply deps.querier - .update_balance("someone", vec![Coin::new(1, "uosmo"), Coin::new(1, "uion")]); + .bank + .update_balance("someone", vec![coin(1, "uosmo"), coin(1, "uion")]); - let admin = "admin"; + let admin = deps.api.addr_make("admin"); + let moderator = deps.api.addr_make("moderator"); let init_msg = InstantiateMsg { pool_asset_configs: vec![ AssetConfig::from_denom_str("uosmo"), @@ -3485,10 +3575,10 @@ mod tests { admin: Some(admin.to_string()), alloyed_asset_subdenom: "usomoion".to_string(), alloyed_asset_normalization_factor: Uint128::one(), - moderator: "moderator".to_string(), + moderator: moderator.to_string(), }; let env = mock_env(); - let info = mock_info(admin, &[]); + let info = message_info(&admin, &[]); // Instantiate the contract. instantiate(deps.as_mut(), env.clone(), info, init_msg).unwrap(); @@ -3509,7 +3599,10 @@ mod tests { } .into(), ), + msg_responses: vec![], }), + payload: Binary::new(vec![]), + gas_used: 0, }, ) .unwrap(); @@ -3530,11 +3623,15 @@ mod tests { fn test_spot_price() { let mut deps = mock_dependencies(); + let someone = deps.api.addr_make("someone"); + let moderator = deps.api.addr_make("moderator"); + // make denom has non-zero total supply deps.querier - .update_balance("someone", vec![Coin::new(1, "uosmo"), Coin::new(1, "uion")]); + .bank + .update_balance(&someone, vec![coin(1, "uosmo"), coin(1, "uion")]); - let admin = "admin"; + let admin = deps.api.addr_make("admin"); let init_msg = InstantiateMsg { pool_asset_configs: vec![ AssetConfig::from_denom_str("uosmo"), @@ -3543,10 +3640,10 @@ mod tests { admin: Some(admin.to_string()), alloyed_asset_subdenom: "uosmoion".to_string(), alloyed_asset_normalization_factor: Uint128::one(), - moderator: "moderator".to_string(), + moderator: moderator.to_string(), }; let env = mock_env(); - let info = mock_info(admin, &[]); + let info = message_info(&admin, &[]); // Instantiate the contract. instantiate(deps.as_mut(), env.clone(), info, init_msg).unwrap(); @@ -3567,7 +3664,10 @@ mod tests { } .into(), ), + msg_responses: vec![], }), + payload: Binary::new(vec![]), + gas_used: 0, }, ) .unwrap(); @@ -3670,12 +3770,15 @@ mod tests { #[test] fn test_spot_price_with_different_norm_factor() { let mut deps = mock_dependencies(); + let someone = deps.api.addr_make("someone"); + let admin = deps.api.addr_make("admin"); + let moderator = deps.api.addr_make("moderator"); // make denom has non-zero total supply deps.querier - .update_balance("someone", vec![Coin::new(1, "tbtc"), Coin::new(1, "nbtc")]); + .bank + .update_balance(&someone, vec![coin(1, "tbtc"), coin(1, "nbtc")]); - let admin = "admin"; let init_msg = InstantiateMsg { pool_asset_configs: vec![ AssetConfig { @@ -3690,10 +3793,10 @@ mod tests { admin: Some(admin.to_string()), alloyed_asset_subdenom: "allbtc".to_string(), alloyed_asset_normalization_factor: Uint128::from(100u128), - moderator: "moderator".to_string(), + moderator: moderator.to_string(), }; let env = mock_env(); - let info = mock_info(admin, &[]); + let info = message_info(&admin, &[]); // Instantiate the contract. instantiate(deps.as_mut(), env.clone(), info, init_msg).unwrap(); @@ -3714,7 +3817,10 @@ mod tests { } .into(), ), + msg_responses: vec![], }), + payload: Binary::new(vec![]), + gas_used: 0, }, ) .unwrap(); @@ -3785,14 +3891,15 @@ mod tests { #[test] fn test_calc_out_amt_given_in() { let mut deps = mock_dependencies(); + let admin = deps.api.addr_make("admin"); + let someone = deps.api.addr_make("someone"); + let moderator = deps.api.addr_make("moderator"); // make denom has non-zero total supply - deps.querier.update_balance( - "someone", - vec![Coin::new(1, "axlusdc"), Coin::new(1, "whusdc")], - ); + deps.querier + .bank + .update_balance(&someone, vec![coin(1, "axlusdc"), coin(1, "whusdc")]); - let admin = "admin"; let init_msg = InstantiateMsg { pool_asset_configs: vec![ AssetConfig::from_denom_str("axlusdc"), @@ -3801,10 +3908,10 @@ mod tests { admin: Some(admin.to_string()), alloyed_asset_subdenom: "alloyedusdc".to_string(), alloyed_asset_normalization_factor: Uint128::one(), - moderator: "moderator".to_string(), + moderator: moderator.to_string(), }; let env = mock_env(); - let info = mock_info(admin, &[]); + let info = message_info(&admin, &[]); // Instantiate the contract. instantiate(deps.as_mut(), env.clone(), info, init_msg).unwrap(); @@ -3825,7 +3932,10 @@ mod tests { } .into(), ), + msg_responses: vec![], }), + payload: Binary::new(vec![]), + gas_used: 0, }, ) .unwrap(); @@ -3835,10 +3945,7 @@ mod tests { execute( deps.as_mut(), env.clone(), - mock_info( - admin, - &[Coin::new(1000, "axlusdc"), Coin::new(2000, "whusdc")], - ), + message_info(&admin, &[coin(1000, "axlusdc"), coin(2000, "whusdc")]), join_pool_msg, ) .unwrap(); @@ -3860,35 +3967,35 @@ mod tests { } in vec![ Case { name: String::from("axlusdc to whusdc - ok"), - token_in: Coin::new(1000, "axlusdc"), + token_in: coin(1000, "axlusdc"), token_out_denom: "whusdc".to_string(), swap_fee: Decimal::zero(), expected: Ok(CalcOutAmtGivenInResponse { - token_out: Coin::new(1000, "whusdc"), + token_out: coin(1000, "whusdc"), }), }, Case { name: String::from("whusdc to axlusdc - ok"), - token_in: Coin::new(1000, "whusdc"), + token_in: coin(1000, "whusdc"), token_out_denom: "axlusdc".to_string(), swap_fee: Decimal::zero(), expected: Ok(CalcOutAmtGivenInResponse { - token_out: Coin::new(1000, "axlusdc"), + token_out: coin(1000, "axlusdc"), }), }, Case { name: String::from("whusdc to axlusdc - token out not enough"), - token_in: Coin::new(1001, "whusdc"), + token_in: coin(1001, "whusdc"), token_out_denom: "axlusdc".to_string(), swap_fee: Decimal::zero(), expected: Err(ContractError::InsufficientPoolAsset { - required: Coin::new(1001, "axlusdc"), - available: Coin::new(1000, "axlusdc"), + required: coin(1001, "axlusdc"), + available: coin(1000, "axlusdc"), }), }, Case { name: String::from("same denom error (pool asset)"), - token_in: Coin::new(1000, "axlusdc"), + token_in: coin(1000, "axlusdc"), token_out_denom: "axlusdc".to_string(), swap_fee: Decimal::zero(), expected: Err(ContractError::SameDenomNotAllowed { @@ -3897,7 +4004,7 @@ mod tests { }, Case { name: String::from("same denom error (alloyed asset)"), - token_in: Coin::new(1000, "alloyedusdc"), + token_in: coin(1000, "alloyedusdc"), token_out_denom: "alloyedusdc".to_string(), swap_fee: Decimal::zero(), expected: Err(ContractError::SameDenomNotAllowed { @@ -3906,53 +4013,53 @@ mod tests { }, Case { name: String::from("alloyedusdc to axlusdc - ok"), - token_in: Coin::new(1000, "alloyedusdc"), + token_in: coin(1000, "alloyedusdc"), token_out_denom: "axlusdc".to_string(), swap_fee: Decimal::zero(), expected: Ok(CalcOutAmtGivenInResponse { - token_out: Coin::new(1000, "axlusdc"), + token_out: coin(1000, "axlusdc"), }), }, Case { name: String::from("alloyedusdc to whusdc - ok"), - token_in: Coin::new(1000, "alloyedusdc"), + token_in: coin(1000, "alloyedusdc"), token_out_denom: "whusdc".to_string(), swap_fee: Decimal::zero(), expected: Ok(CalcOutAmtGivenInResponse { - token_out: Coin::new(1000, "whusdc"), + token_out: coin(1000, "whusdc"), }), }, Case { name: String::from("alloyedusdc to axlusdc - token out not enough"), - token_in: Coin::new(1001, "alloyedusdc"), + token_in: coin(1001, "alloyedusdc"), token_out_denom: "axlusdc".to_string(), swap_fee: Decimal::zero(), expected: Err(ContractError::InsufficientPoolAsset { - required: Coin::new(1001, "axlusdc"), - available: Coin::new(1000, "axlusdc"), + required: coin(1001, "axlusdc"), + available: coin(1000, "axlusdc"), }), }, Case { name: String::from("axlusdc to alloyedusdc - ok"), - token_in: Coin::new(1000, "axlusdc"), + token_in: coin(1000, "axlusdc"), token_out_denom: "alloyedusdc".to_string(), swap_fee: Decimal::zero(), expected: Ok(CalcOutAmtGivenInResponse { - token_out: Coin::new(1000, "alloyedusdc"), + token_out: coin(1000, "alloyedusdc"), }), }, Case { name: String::from("whusdc to alloyedusdc - ok"), - token_in: Coin::new(1000, "whusdc"), + token_in: coin(1000, "whusdc"), token_out_denom: "alloyedusdc".to_string(), swap_fee: Decimal::zero(), expected: Ok(CalcOutAmtGivenInResponse { - token_out: Coin::new(1000, "alloyedusdc"), + token_out: coin(1000, "alloyedusdc"), }), }, Case { name: String::from("invalid swap fee"), - token_in: Coin::new(1000, "axlusdc"), + token_in: coin(1000, "axlusdc"), token_out_denom: "whusdc".to_string(), swap_fee: Decimal::percent(1), expected: Err(ContractError::InvalidSwapFee { @@ -3962,7 +4069,7 @@ mod tests { }, Case { name: String::from("invalid swap fee (alloyed asset as token in)"), - token_in: Coin::new(1000, "alloyedusdc"), + token_in: coin(1000, "alloyedusdc"), token_out_denom: "whusdc".to_string(), swap_fee: Decimal::percent(1), expected: Err(ContractError::InvalidSwapFee { @@ -3972,7 +4079,7 @@ mod tests { }, Case { name: String::from("invalid swap fee (alloyed asset as token out)"), - token_in: Coin::new(1000, "axlusdc"), + token_in: coin(1000, "axlusdc"), token_out_denom: "alloyedusdc".to_string(), swap_fee: Decimal::percent(2), expected: Err(ContractError::InvalidSwapFee { @@ -3999,14 +4106,15 @@ mod tests { #[test] fn test_calc_in_amt_given_out() { let mut deps = mock_dependencies(); + let someone = deps.api.addr_make("someone"); + let admin = deps.api.addr_make("admin"); + let moderator = deps.api.addr_make("moderator"); // make denom has non-zero total supply - deps.querier.update_balance( - "someone", - vec![Coin::new(1, "axlusdc"), Coin::new(1, "whusdc")], - ); + deps.querier + .bank + .update_balance(&someone, vec![coin(1, "axlusdc"), coin(1, "whusdc")]); - let admin = "admin"; let init_msg = InstantiateMsg { pool_asset_configs: vec![ AssetConfig::from_denom_str("axlusdc"), @@ -4015,10 +4123,10 @@ mod tests { admin: Some(admin.to_string()), alloyed_asset_subdenom: "alloyedusdc".to_string(), alloyed_asset_normalization_factor: Uint128::one(), - moderator: "moderator".to_string(), + moderator: moderator.to_string(), }; let env = mock_env(); - let info = mock_info(admin, &[]); + let info = message_info(&admin, &[]); // Instantiate the contract. instantiate(deps.as_mut(), env.clone(), info, init_msg).unwrap(); @@ -4039,7 +4147,10 @@ mod tests { } .into(), ), + msg_responses: vec![], }), + payload: Binary::new(vec![]), + gas_used: 0, }, ) .unwrap(); @@ -4049,10 +4160,7 @@ mod tests { execute( deps.as_mut(), env.clone(), - mock_info( - admin, - &[Coin::new(1000, "axlusdc"), Coin::new(2000, "whusdc")], - ), + message_info(&admin, &[coin(1000, "axlusdc"), coin(2000, "whusdc")]), join_pool_msg, ) .unwrap(); @@ -4075,35 +4183,35 @@ mod tests { Case { name: String::from("axlusdc to whusdc - ok"), token_in_denom: "axlusdc".to_string(), - token_out: Coin::new(1000, "whusdc"), + token_out: coin(1000, "whusdc"), swap_fee: Decimal::zero(), expected: Ok(CalcInAmtGivenOutResponse { - token_in: Coin::new(1000, "axlusdc"), + token_in: coin(1000, "axlusdc"), }), }, Case { name: String::from("whusdc to axlusdc - ok"), token_in_denom: "whusdc".to_string(), - token_out: Coin::new(1000, "axlusdc"), + token_out: coin(1000, "axlusdc"), swap_fee: Decimal::zero(), expected: Ok(CalcInAmtGivenOutResponse { - token_in: Coin::new(1000, "whusdc"), + token_in: coin(1000, "whusdc"), }), }, Case { name: String::from("whusdc to axlusdc - token out not enough"), token_in_denom: "whusdc".to_string(), - token_out: Coin::new(1001, "axlusdc"), + token_out: coin(1001, "axlusdc"), swap_fee: Decimal::zero(), expected: Err(ContractError::InsufficientPoolAsset { - required: Coin::new(1001, "axlusdc"), - available: Coin::new(1000, "axlusdc"), + required: coin(1001, "axlusdc"), + available: coin(1000, "axlusdc"), }), }, Case { name: String::from("same denom error (pool asset)"), token_in_denom: "axlusdc".to_string(), - token_out: Coin::new(1000, "axlusdc"), + token_out: coin(1000, "axlusdc"), swap_fee: Decimal::zero(), expected: Err(ContractError::SameDenomNotAllowed { denom: "axlusdc".to_string(), @@ -4112,7 +4220,7 @@ mod tests { Case { name: String::from("same denom error (alloyed asset)"), token_in_denom: "alloyedusdc".to_string(), - token_out: Coin::new(1000, "alloyedusdc"), + token_out: coin(1000, "alloyedusdc"), swap_fee: Decimal::zero(), expected: Err(ContractError::SameDenomNotAllowed { denom: "alloyedusdc".to_string(), @@ -4121,44 +4229,44 @@ mod tests { Case { name: String::from("alloyedusdc to axlusdc - ok"), token_in_denom: "alloyedusdc".to_string(), - token_out: Coin::new(1000, "axlusdc"), + token_out: coin(1000, "axlusdc"), swap_fee: Decimal::zero(), expected: Ok(CalcInAmtGivenOutResponse { - token_in: Coin::new(1000, "alloyedusdc"), + token_in: coin(1000, "alloyedusdc"), }), }, Case { name: String::from("alloyedusdc to whusdc - ok"), token_in_denom: "alloyedusdc".to_string(), - token_out: Coin::new(1000, "whusdc"), + token_out: coin(1000, "whusdc"), swap_fee: Decimal::zero(), expected: Ok(CalcInAmtGivenOutResponse { - token_in: Coin::new(1000, "alloyedusdc"), + token_in: coin(1000, "alloyedusdc"), }), }, Case { name: String::from("alloyedusdc to axlusdc - token out not enough"), token_in_denom: "alloyedusdc".to_string(), - token_out: Coin::new(1001, "axlusdc"), + token_out: coin(1001, "axlusdc"), swap_fee: Decimal::zero(), expected: Err(ContractError::InsufficientPoolAsset { - required: Coin::new(1001, "axlusdc"), - available: Coin::new(1000, "axlusdc"), + required: coin(1001, "axlusdc"), + available: coin(1000, "axlusdc"), }), }, Case { name: String::from("pool asset to alloyed asset - ok"), token_in_denom: "axlusdc".to_string(), - token_out: Coin::new(1000, "alloyedusdc"), + token_out: coin(1000, "alloyedusdc"), swap_fee: Decimal::zero(), expected: Ok(CalcInAmtGivenOutResponse { - token_in: Coin::new(1000, "axlusdc"), + token_in: coin(1000, "axlusdc"), }), }, Case { name: String::from("invalid swap fee"), token_in_denom: "whusdc".to_string(), - token_out: Coin::new(1000, "axlusdc"), + token_out: coin(1000, "axlusdc"), swap_fee: Decimal::percent(1), expected: Err(ContractError::InvalidSwapFee { expected: Decimal::zero(), @@ -4168,7 +4276,7 @@ mod tests { Case { name: String::from("invalid swap fee (alloyed asset as token in)"), token_in_denom: "alloyedusdc".to_string(), - token_out: Coin::new(1000, "axlusdc"), + token_out: coin(1000, "axlusdc"), swap_fee: Decimal::percent(1), expected: Err(ContractError::InvalidSwapFee { expected: Decimal::zero(), @@ -4178,7 +4286,7 @@ mod tests { Case { name: String::from("invalid swap fee (alloyed asset as token out)"), token_in_denom: "whusdc".to_string(), - token_out: Coin::new(1000, "alloyedusdc"), + token_out: coin(1000, "alloyedusdc"), swap_fee: Decimal::percent(2), expected: Err(ContractError::InvalidSwapFee { expected: Decimal::zero(), @@ -4204,14 +4312,15 @@ mod tests { #[test] fn test_rescale_normalization_factor() { let mut deps = mock_dependencies(); + let someone = deps.api.addr_make("someone"); + let admin = deps.api.addr_make("admin"); + let moderator = deps.api.addr_make("moderator"); // make denom has non-zero total supply - deps.querier.update_balance( - "someone", - vec![Coin::new(1, "axlusdc"), Coin::new(1, "whusdc")], - ); + deps.querier + .bank + .update_balance(&someone, vec![coin(1, "axlusdc"), coin(1, "whusdc")]); - let admin = "admin"; let init_msg = InstantiateMsg { pool_asset_configs: vec![ AssetConfig::from_denom_str("axlusdc"), @@ -4220,10 +4329,10 @@ mod tests { admin: Some(admin.to_string()), alloyed_asset_subdenom: "alloyedusdc".to_string(), alloyed_asset_normalization_factor: Uint128::from(100u128), - moderator: "moderator".to_string(), + moderator: moderator.to_string(), }; let env = mock_env(); - let info = mock_info(admin, &[]); + let info = message_info(&admin, &[]); // Instantiate the contract. instantiate(deps.as_mut(), env.clone(), info, init_msg).unwrap(); @@ -4244,7 +4353,10 @@ mod tests { } .into(), ), + msg_responses: vec![], }), + payload: Binary::new(vec![]), + gas_used: 0, }, ) .unwrap(); @@ -4280,7 +4392,7 @@ mod tests { execute( deps.as_mut(), env.clone(), - mock_info(admin, &[]), + message_info(&admin, &[]), rescale_msg, ) .unwrap(); @@ -4322,7 +4434,7 @@ mod tests { execute( deps.as_mut(), env.clone(), - mock_info(admin, &[]), + message_info(&admin, &[]), rescale_msg, ) .unwrap(); @@ -4360,22 +4472,23 @@ mod tests { fn test_asset_group() { let mut deps = mock_dependencies(); let env = mock_env(); - let admin = "admin"; + let admin = deps.api.addr_make("admin"); + let moderator = deps.api.addr_make("moderator"); // Setup balance for each asset - deps.querier.update_balance( + deps.querier.bank.update_balance( env.contract.address.clone(), vec![ - Coin::new(1000000, "asset1"), - Coin::new(1000000, "asset2"), - Coin::new(1000000, "asset3"), + coin(1000000, "asset1"), + coin(1000000, "asset2"), + coin(1000000, "asset3"), ], ); // Initialize the contract let instantiate_msg = InstantiateMsg { admin: Some(admin.to_string()), - moderator: "moderator".to_string(), + moderator: moderator.to_string(), pool_asset_configs: vec![ AssetConfig { denom: "asset1".to_string(), @@ -4394,7 +4507,7 @@ mod tests { alloyed_asset_normalization_factor: Uint128::from(1000000u128), }; - let info = mock_info(admin, &[]); + let info = message_info(&admin, &[]); instantiate(deps.as_mut(), env.clone(), info.clone(), instantiate_msg).unwrap(); // Create asset group @@ -4404,7 +4517,8 @@ mod tests { }); // Test non-admin trying to create asset group - let non_admin_info = mock_info("non_admin", &[]); + let non_admin = deps.api.addr_make("non_admin"); + let non_admin_info = message_info(&non_admin, &[]); let non_admin_create_msg = ContractExecMsg::Transmuter(ExecMsg::CreateAssetGroup { label: "group1".to_string(), denoms: vec!["asset1".to_string(), "asset2".to_string()], @@ -4458,7 +4572,7 @@ mod tests { }, }); - let register_limiter_info = mock_info("admin", &[]); + let register_limiter_info = message_info(&admin, &[]); let err = execute( deps.as_mut(), env.clone(), @@ -4475,7 +4589,7 @@ mod tests { denoms: vec!["asset3".to_string()], }); - let create_asset_group_info2 = mock_info("admin", &[]); + let create_asset_group_info2 = message_info(&admin, &[]); let res2 = execute( deps.as_mut(), env.clone(), @@ -4567,7 +4681,7 @@ mod tests { denoms: vec!["asset1".to_string(), "non_existing_asset".to_string()], }); - let admin_info = mock_info(admin, &[]); + let admin_info = message_info(&admin, &[]); let err = execute( deps.as_mut(), env.clone(), @@ -4608,7 +4722,7 @@ mod tests { }); // Try to remove the group with a non-admin account - let non_admin_info = mock_info("non_admin", &[]); + let non_admin_info = message_info(&non_admin, &[]); let err = execute( deps.as_mut(), env.clone(), @@ -4620,7 +4734,7 @@ mod tests { assert_eq!(err, ContractError::Unauthorized {}); // Remove the group with the admin account - let admin_info = mock_info(admin, &[]); + let admin_info = message_info(&admin, &[]); let res = execute(deps.as_mut(), env.clone(), admin_info, remove_group_msg).unwrap(); assert_eq!( @@ -4657,7 +4771,7 @@ mod tests { label: "non_existent_group".to_string(), }); - let admin_info = mock_info(admin, &[]); + let admin_info = message_info(&admin, &[]); let err = execute( deps.as_mut(), env.clone(), @@ -4678,14 +4792,14 @@ mod tests { fn test_mark_corrupted_scopes() { let mut deps = mock_dependencies(); let env = mock_env(); - let admin = "admin"; - let moderator = "moderator"; - let user = "user"; + let admin = deps.api.addr_make("admin"); + let moderator = deps.api.addr_make("moderator"); + let user = deps.api.addr_make("user"); - // Add supply for denoms using deps.querier.update_balance - deps.querier.update_balance( + // Add supply for denoms using deps.querier.bank.update_balance + deps.querier.bank.update_balance( env.contract.address.clone(), - vec![Coin::new(1000000, "asset1"), Coin::new(2000000, "asset2")], + vec![coin(1000000, "asset1"), coin(2000000, "asset2")], ); // Initialize the contract @@ -4705,14 +4819,14 @@ mod tests { alloyed_asset_subdenom: "alloyed".to_string(), alloyed_asset_normalization_factor: Uint128::new(1), }; - let info = mock_info(admin, &[]); + let info = message_info(&admin, &[]); instantiate(deps.as_mut(), env.clone(), info, init_msg).unwrap(); // Mark corrupted scopes let mark_corrupted_scopes_msg = ContractExecMsg::Transmuter(ExecMsg::MarkCorruptedScopes { scopes: vec![Scope::Denom("asset1".to_string())], }); - let moderator_info = mock_info(moderator, &[]); + let moderator_info = message_info(&moderator, &[]); let res = execute( deps.as_mut(), env.clone(), @@ -4737,7 +4851,7 @@ mod tests { assert_eq!(query_res.corrupted_scopes, vec![Scope::denom("asset1")]); // Try to mark corrupted scopes as a non-moderator (should fail) - let user_info = mock_info(user, &[]); + let user_info = message_info(&user, &[]); let unauthorized_mark_msg = ContractExecMsg::Transmuter(ExecMsg::MarkCorruptedScopes { scopes: vec![Scope::denom("asset2")], }); @@ -4750,7 +4864,7 @@ mod tests { label: "group1".to_string(), denoms: vec!["asset1".to_string(), "asset2".to_string()], }); - let admin_info = mock_info(admin, &[]); + let admin_info = message_info(&admin, &[]); let res = execute( deps.as_mut(), env.clone(), @@ -4769,7 +4883,7 @@ mod tests { ); // Test mark_asset_group_as_corrupted - let moderator_info = mock_info(moderator, &[]); + let moderator_info = message_info(&moderator, &[]); let mark_group_corrupted_msg = ContractExecMsg::Transmuter(ExecMsg::MarkCorruptedScopes { scopes: vec![Scope::asset_group("group1")], }); diff --git a/contracts/transmuter/src/lib.rs b/contracts/transmuter/src/lib.rs index af874d1..e11a440 100644 --- a/contracts/transmuter/src/lib.rs +++ b/contracts/transmuter/src/lib.rs @@ -28,7 +28,7 @@ mod entry_points { use crate::migrations; use crate::sudo::SudoMsg; - const CONTRACT: Transmuter = Transmuter::default(); + const CONTRACT: Transmuter = Transmuter::new(); macro_rules! ensure_active_status { ($msg:expr, $deps:expr, $env:expr, except: $pattern:pat) => { diff --git a/contracts/transmuter/src/limiter.rs b/contracts/transmuter/src/limiter.rs index a517ac1..dcd9041 100644 --- a/contracts/transmuter/src/limiter.rs +++ b/contracts/transmuter/src/limiter.rs @@ -320,13 +320,13 @@ pub enum LimiterParams { }, } -pub struct Limiters<'a> { +pub struct Limiters { /// Map of (scope, label) -> Limiter - limiters: Map<'a, (&'a str, &'a str), Limiter>, + limiters: Map<(&'static str, &'static str), Limiter>, } -impl<'a> Limiters<'a> { - pub const fn new(limiters_namespace: &'a str) -> Self { +impl Limiters { + pub const fn new(limiters_namespace: &'static str) -> Self { Self { limiters: Map::new(limiters_namespace), } @@ -1362,9 +1362,7 @@ mod tests { assert_eq!( err, - ContractError::DivideByZeroError(DivideByZeroError::new(Uint64::from( - 604_800_000_000u64 - ))) + ContractError::DivideByZeroError(DivideByZeroError::new()) ); } diff --git a/contracts/transmuter/src/math.rs b/contracts/transmuter/src/math.rs index 90d2d73..fead19a 100644 --- a/contracts/transmuter/src/math.rs +++ b/contracts/transmuter/src/math.rs @@ -176,7 +176,7 @@ mod tests { 5u128, 20u128, 0u128, - Err(MathError::DivideByZeroError(DivideByZeroError { operand: String::from("100")})) + Err(MathError::DivideByZeroError(DivideByZeroError::new())) )] #[case( 1000u128, diff --git a/contracts/transmuter/src/migrations/v4_0_0.rs b/contracts/transmuter/src/migrations/v4_0_0.rs index 0ba6a42..54bb0ce 100644 --- a/contracts/transmuter/src/migrations/v4_0_0.rs +++ b/contracts/transmuter/src/migrations/v4_0_0.rs @@ -40,15 +40,14 @@ pub fn execute_migration(deps: DepsMut) -> Result { ); // add asset groups to the pool - let pool_v3: TransmuterPoolV3 = - Item::<'_, TransmuterPoolV3>::new(key::POOL).load(deps.storage)?; + let pool_v3: TransmuterPoolV3 = Item::::new(key::POOL).load(deps.storage)?; let pool_v4 = TransmuterPool { pool_assets: pool_v3.pool_assets, asset_groups: BTreeMap::new(), }; - Item::<'_, TransmuterPool>::new(key::POOL).save(deps.storage, &pool_v4)?; + Item::::new(key::POOL).save(deps.storage, &pool_v4)?; // Set the contract version to the target version after successful migration cw2::set_contract_version(deps.storage, CONTRACT_NAME, TO_VERSION)?; @@ -113,7 +112,7 @@ mod tests { let res = execute_migration(deps.as_mut()).unwrap(); - let pool = Item::<'_, TransmuterPool>::new(key::POOL) + let pool = Item::::new(key::POOL) .load(&deps.storage) .unwrap(); diff --git a/contracts/transmuter/src/role/admin.rs b/contracts/transmuter/src/role/admin.rs index fcdedb1..7704a65 100644 --- a/contracts/transmuter/src/role/admin.rs +++ b/contracts/transmuter/src/role/admin.rs @@ -4,8 +4,8 @@ use cw_storage_plus::Item; use crate::ContractError; -pub struct Admin<'a> { - state: Item<'a, AdminState>, +pub struct Admin { + state: Item, } /// State of the admin to be stored in the contract storage @@ -15,8 +15,8 @@ pub enum AdminState { Transferring { current: Addr, candidate: Addr }, } -impl<'a> Admin<'a> { - pub const fn new(namespace: &'a str) -> Self { +impl Admin { + pub const fn new(namespace: &'static str) -> Self { Self { state: Item::new(namespace), } diff --git a/contracts/transmuter/src/role/mod.rs b/contracts/transmuter/src/role/mod.rs index 51569e7..8aa1777 100644 --- a/contracts/transmuter/src/role/mod.rs +++ b/contracts/transmuter/src/role/mod.rs @@ -5,13 +5,13 @@ use crate::{ensure_admin_authority, ContractError}; pub mod admin; pub mod moderator; -pub struct Role<'a> { - pub admin: admin::Admin<'a>, - pub moderator: moderator::Moderator<'a>, +pub struct Role { + pub admin: admin::Admin, + pub moderator: moderator::Moderator, } -impl<'a> Role<'a> { - pub const fn new(admin_namespace: &'a str, moderator_namespace: &'a str) -> Self { +impl Role { + pub const fn new(admin_namespace: &'static str, moderator_namespace: &'static str) -> Self { Role { admin: admin::Admin::new(admin_namespace), moderator: moderator::Moderator::new(moderator_namespace), diff --git a/contracts/transmuter/src/role/moderator.rs b/contracts/transmuter/src/role/moderator.rs index 72c066f..666033c 100644 --- a/contracts/transmuter/src/role/moderator.rs +++ b/contracts/transmuter/src/role/moderator.rs @@ -3,12 +3,12 @@ use cw_storage_plus::Item; use crate::ContractError; -pub struct Moderator<'a> { - moderator: Item<'a, Addr>, +pub struct Moderator { + moderator: Item, } -impl<'a> Moderator<'a> { - pub const fn new(namespace: &'a str) -> Self { +impl Moderator { + pub const fn new(namespace: &'static str) -> Self { Self { moderator: Item::new(namespace), } diff --git a/contracts/transmuter/src/sudo.rs b/contracts/transmuter/src/sudo.rs index 0b7116e..74319bb 100644 --- a/contracts/transmuter/src/sudo.rs +++ b/contracts/transmuter/src/sudo.rs @@ -176,8 +176,9 @@ mod tests { swap::{SwapExactAmountInResponseData, SwapExactAmountOutResponseData}, }; use cosmwasm_std::{ - testing::{mock_dependencies, mock_env, mock_info, MOCK_CONTRACT_ADDR}, - to_json_binary, BankMsg, Reply, SubMsgResponse, SubMsgResult, + coin, + testing::{message_info, mock_dependencies, mock_env, mock_info, MOCK_CONTRACT_ADDR}, + to_json_binary, BankMsg, Binary, Reply, SubMsgResponse, SubMsgResult, }; use osmosis_std::types::osmosis::tokenfactory::v1beta1::{ MsgBurn, MsgCreateDenomResponse, MsgMint, @@ -186,15 +187,16 @@ mod tests { #[test] fn test_swap_exact_amount_in() { let mut deps = mock_dependencies(); + let someone = deps.api.addr_make("someone"); + let admin = deps.api.addr_make("admin"); + let user = deps.api.addr_make("user"); + let moderator = deps.api.addr_make("moderator"); // make denom has non-zero total supply - deps.querier.update_balance( - "someone", - vec![Coin::new(1, "axlusdc"), Coin::new(1, "whusdc")], - ); + deps.querier + .bank + .update_balance(&someone, vec![coin(1, "axlusdc"), coin(1, "whusdc")]); - let admin = "admin"; - let user = "user"; let init_msg = InstantiateMsg { pool_asset_configs: vec![ AssetConfig::from_denom_str("axlusdc"), @@ -203,10 +205,10 @@ mod tests { alloyed_asset_subdenom: "uusdc".to_string(), alloyed_asset_normalization_factor: Uint128::one(), admin: Some(admin.to_string()), - moderator: "moderator".to_string(), + moderator: moderator.to_string(), }; let env = mock_env(); - let info = mock_info(admin, &[]); + let info = message_info(&admin, &[]); // Instantiate the contract. instantiate(deps.as_mut(), env.clone(), info, init_msg).unwrap(); @@ -227,7 +229,10 @@ mod tests { } .into(), ), + msg_responses: vec![], }), + payload: Binary::new(vec![]), + gas_used: 0, }, ) .unwrap(); @@ -236,11 +241,11 @@ mod tests { execute( deps.as_mut(), env.clone(), - mock_info( - user, + message_info( + &user, &[ - Coin::new(1_000_000_000_000, "axlusdc"), - Coin::new(1_000_000_000_000, "whusdc"), + coin(1_000_000_000_000, "axlusdc"), + coin(1_000_000_000_000, "whusdc"), ], ), join_pool_msg, @@ -250,7 +255,7 @@ mod tests { // Test swap exact amount in with 0 amount in should error with ZeroValueOperation let swap_msg = SudoMsg::SwapExactAmountIn { sender: user.to_string(), - token_in: Coin::new(0, "axlusdc".to_string()), + token_in: coin(0, "axlusdc".to_string()), token_out_denom: "whusdc".to_string(), token_out_min_amount: Uint128::from(0u128), swap_fee: Decimal::zero(), @@ -262,7 +267,7 @@ mod tests { // Test swap exact amount in with only pool assets let swap_msg = SudoMsg::SwapExactAmountIn { sender: user.to_string(), - token_in: Coin::new(500, "axlusdc".to_string()), + token_in: coin(500, "axlusdc".to_string()), token_out_denom: "whusdc".to_string(), token_out_min_amount: Uint128::from(500u128), swap_fee: Decimal::zero(), @@ -274,7 +279,7 @@ mod tests { .add_attribute("method", "swap_exact_amount_in") .add_message(BankMsg::Send { to_address: user.to_string(), - amount: vec![Coin::new(500, "whusdc".to_string())], + amount: vec![coin(500, "whusdc".to_string())], }) .set_data( to_json_binary(&SwapExactAmountInResponseData { @@ -287,10 +292,11 @@ mod tests { // Test swap with token in as alloyed asset deps.querier - .update_balance(MOCK_CONTRACT_ADDR, vec![Coin::new(500, alloyed_denom)]); + .bank + .update_balance(MOCK_CONTRACT_ADDR, vec![coin(500, alloyed_denom)]); let swap_msg = SudoMsg::SwapExactAmountIn { sender: user.to_string(), - token_in: Coin::new(500, alloyed_denom), + token_in: coin(500, alloyed_denom), token_out_denom: "whusdc".to_string(), token_out_min_amount: Uint128::from(500u128), swap_fee: Decimal::zero(), @@ -301,13 +307,13 @@ mod tests { let expected = Response::new() .add_attribute("method", "swap_exact_amount_in") .add_message(MsgBurn { - amount: Some(Coin::new(500, alloyed_denom).into()), + amount: Some(coin(500, alloyed_denom).into()), sender: env.contract.address.to_string(), burn_from_address: env.contract.address.to_string(), }) .add_message(BankMsg::Send { to_address: user.to_string(), - amount: vec![Coin::new(500, "whusdc".to_string())], + amount: vec![coin(500, "whusdc".to_string())], }) .set_data( to_json_binary(&SwapExactAmountInResponseData { @@ -321,7 +327,7 @@ mod tests { // Test swap with token out as alloyed asset let swap_msg = SudoMsg::SwapExactAmountIn { sender: user.to_string(), - token_in: Coin::new(500, "whusdc".to_string()), + token_in: coin(500, "whusdc".to_string()), token_out_denom: alloyed_denom.to_string(), token_out_min_amount: Uint128::from(500u128), swap_fee: Decimal::zero(), @@ -333,7 +339,7 @@ mod tests { .add_attribute("method", "swap_exact_amount_in") .add_message(MsgMint { sender: env.contract.address.to_string(), - amount: Some(Coin::new(500, alloyed_denom).into()), + amount: Some(coin(500, alloyed_denom).into()), mint_to_address: user.to_string(), }) .set_data( @@ -348,7 +354,7 @@ mod tests { // Test case for ensure token_out amount is greater than or equal to token_out_min_amount let swap_msg = SudoMsg::SwapExactAmountIn { sender: user.to_string(), - token_in: Coin::new(500, "whusdc".to_string()), + token_in: coin(500, "whusdc".to_string()), token_out_denom: "axlusdc".to_string(), token_out_min_amount: Uint128::from(1000u128), // set min amount greater than token_in swap_fee: Decimal::zero(), @@ -367,7 +373,7 @@ mod tests { // Test case for ensure token_out amount is greater than or equal to token_out_min_amount but token_in is alloyed asset let swap_msg = SudoMsg::SwapExactAmountIn { sender: user.to_string(), - token_in: Coin::new(500, alloyed_denom.to_string()), + token_in: coin(500, alloyed_denom.to_string()), token_out_denom: "axlusdc".to_string(), token_out_min_amount: Uint128::from(1000u128), // set min amount greater than token_in swap_fee: Decimal::zero(), @@ -386,7 +392,7 @@ mod tests { // Test case for ensure token_out amount is greater than or equal to token_out_min_amount but token_out is alloyed asset let swap_msg = SudoMsg::SwapExactAmountIn { sender: user.to_string(), - token_in: Coin::new(500, "whusdc".to_string()), + token_in: coin(500, "whusdc".to_string()), token_out_denom: alloyed_denom.to_string(), token_out_min_amount: Uint128::from(1000u128), // set min amount greater than token_in swap_fee: Decimal::zero(), @@ -406,15 +412,16 @@ mod tests { #[test] fn test_swap_exact_token_out() { let mut deps = mock_dependencies(); + let admin = deps.api.addr_make("admin"); + let user = deps.api.addr_make("user"); + let someone = deps.api.addr_make("someone"); + let moderator = deps.api.addr_make("moderator"); // make denom has non-zero total supply - deps.querier.update_balance( - "someone", - vec![Coin::new(1, "axlusdc"), Coin::new(1, "whusdc")], - ); + deps.querier + .bank + .update_balance(&someone, vec![coin(1, "axlusdc"), coin(1, "whusdc")]); - let admin = "admin"; - let user = "user"; let init_msg = InstantiateMsg { pool_asset_configs: vec![ AssetConfig::from_denom_str("axlusdc"), @@ -423,10 +430,10 @@ mod tests { alloyed_asset_subdenom: "uusdc".to_string(), alloyed_asset_normalization_factor: Uint128::one(), admin: Some(admin.to_string()), - moderator: "moderator".to_string(), + moderator: moderator.to_string(), }; let env = mock_env(); - let info = mock_info(admin, &[]); + let info = message_info(&admin, &[]); // Instantiate the contract. instantiate(deps.as_mut(), env.clone(), info, init_msg).unwrap(); @@ -447,7 +454,10 @@ mod tests { } .into(), ), + msg_responses: vec![], }), + payload: Binary::new(vec![]), + gas_used: 0, }, ) .unwrap(); @@ -456,11 +466,11 @@ mod tests { execute( deps.as_mut(), env.clone(), - mock_info( - user, + message_info( + &user, &[ - Coin::new(1_000_000_000_000, "axlusdc"), - Coin::new(1_000_000_000_000, "whusdc"), + coin(1_000_000_000_000, "axlusdc"), + coin(1_000_000_000_000, "whusdc"), ], ), join_pool_msg, @@ -471,7 +481,7 @@ mod tests { let swap_msg = SudoMsg::SwapExactAmountOut { sender: user.to_string(), token_in_denom: "whusdc".to_string(), - token_out: Coin::new(0, "axlusdc".to_string()), + token_out: coin(0, "axlusdc".to_string()), token_in_max_amount: Uint128::from(0u128), swap_fee: Decimal::zero(), }; @@ -484,7 +494,7 @@ mod tests { sender: user.to_string(), token_in_denom: "axlusdc".to_string(), token_in_max_amount: Uint128::from(500u128), - token_out: Coin::new(500, "whusdc".to_string()), + token_out: coin(500, "whusdc".to_string()), swap_fee: Decimal::zero(), }; @@ -494,7 +504,7 @@ mod tests { .add_attribute("method", "swap_exact_amount_out") .add_message(BankMsg::Send { to_address: user.to_string(), - amount: vec![Coin::new(500, "whusdc".to_string())], + amount: vec![coin(500, "whusdc".to_string())], }) .set_data( to_json_binary(&SwapExactAmountOutResponseData { @@ -507,13 +517,14 @@ mod tests { // Test swap with token in as alloyed asset deps.querier - .update_balance(MOCK_CONTRACT_ADDR, vec![Coin::new(500, alloyed_denom)]); + .bank + .update_balance(MOCK_CONTRACT_ADDR, vec![coin(500, alloyed_denom)]); let swap_msg = SudoMsg::SwapExactAmountOut { sender: user.to_string(), token_in_denom: alloyed_denom.to_string(), token_in_max_amount: Uint128::from(500u128), - token_out: Coin::new(500, "whusdc".to_string()), + token_out: coin(500, "whusdc".to_string()), swap_fee: Decimal::zero(), }; @@ -522,13 +533,13 @@ mod tests { let expected = Response::new() .add_attribute("method", "swap_exact_amount_out") .add_message(MsgBurn { - amount: Some(Coin::new(500, alloyed_denom).into()), + amount: Some(coin(500, alloyed_denom).into()), sender: env.contract.address.to_string(), burn_from_address: env.contract.address.to_string(), }) .add_message(BankMsg::Send { to_address: user.to_string(), - amount: vec![Coin::new(500, "whusdc".to_string())], + amount: vec![coin(500, "whusdc".to_string())], }) .set_data( to_json_binary(&SwapExactAmountOutResponseData { @@ -544,7 +555,7 @@ mod tests { sender: user.to_string(), token_in_denom: "whusdc".to_string(), token_in_max_amount: Uint128::from(500u128), - token_out: Coin::new(500, alloyed_denom.to_string()), + token_out: coin(500, alloyed_denom.to_string()), swap_fee: Decimal::zero(), }; @@ -554,7 +565,7 @@ mod tests { .add_attribute("method", "swap_exact_amount_out") .add_message(MsgMint { sender: env.contract.address.to_string(), - amount: Some(Coin::new(500, alloyed_denom).into()), + amount: Some(coin(500, alloyed_denom).into()), mint_to_address: user.to_string(), }) .set_data( @@ -571,7 +582,7 @@ mod tests { sender: user.to_string(), token_in_denom: "whusdc".to_string(), token_in_max_amount: Uint128::from(500u128), // set max amount less than token_out - token_out: Coin::new(1000, "axlusdc".to_string()), + token_out: coin(1000, "axlusdc".to_string()), swap_fee: Decimal::zero(), }; @@ -590,7 +601,7 @@ mod tests { sender: user.to_string(), token_in_denom: alloyed_denom.to_string(), token_in_max_amount: Uint128::from(500u128), // set max amount less than token_out - token_out: Coin::new(1000, "axlusdc".to_string()), + token_out: coin(1000, "axlusdc".to_string()), swap_fee: Decimal::zero(), }; @@ -609,7 +620,7 @@ mod tests { sender: user.to_string(), token_in_denom: "whusdc".to_string(), token_in_max_amount: Uint128::from(500u128), // set max amount less than token_out - token_out: Coin::new(1000, alloyed_denom.to_string()), + token_out: coin(1000, alloyed_denom.to_string()), swap_fee: Decimal::zero(), }; diff --git a/contracts/transmuter/src/swap.rs b/contracts/transmuter/src/swap.rs index 081f221..4928ae5 100644 --- a/contracts/transmuter/src/swap.rs +++ b/contracts/transmuter/src/swap.rs @@ -2,8 +2,8 @@ use std::collections::{BTreeMap, HashMap}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - ensure, ensure_eq, to_json_binary, Addr, BankMsg, Coin, Decimal, Deps, DepsMut, Env, Response, - StdError, Storage, Uint128, + coin, ensure, ensure_eq, to_json_binary, Addr, BankMsg, Coin, Decimal, Deps, DepsMut, Env, + Response, StdError, Storage, Uint128, }; use osmosis_std::types::osmosis::tokenfactory::v1beta1::{MsgBurn, MsgMint}; use serde::Serialize; @@ -20,7 +20,7 @@ use crate::{ /// Swap fee is hardcoded to zero intentionally. pub const SWAP_FEE: Decimal = Decimal::zero(); -impl Transmuter<'_> { +impl Transmuter { /// Getting the [SwapVariant] of the swap operation /// assuming the swap token is not pub fn swap_variant( @@ -100,7 +100,7 @@ impl Transmuter<'_> { token_out_amount, self.alloyed_asset.get_normalization_factor(deps.storage)?, )?; - let tokens_in = vec![Coin::new(in_amount.u128(), token_in_denom)]; + let tokens_in = vec![coin(in_amount.u128(), token_in_denom)]; let response = set_data_if_sudo( response, @@ -151,7 +151,7 @@ impl Transmuter<'_> { self.pool.save(deps.storage, &pool)?; - let alloyed_asset_out = Coin::new( + let alloyed_asset_out = coin( out_amount.u128(), self.alloyed_asset.get_alloyed_denom(deps.storage)?, ); @@ -202,7 +202,7 @@ impl Transmuter<'_> { }, )?; - let tokens_out = vec![Coin::new(out_amount.u128(), token_out_denom)]; + let tokens_out = vec![coin(out_amount.u128(), token_out_denom)]; (token_in_amount, tokens_out, response) } @@ -359,7 +359,7 @@ impl Transmuter<'_> { amount: tokens_out, }; - let alloyed_asset_to_burn = Coin::new( + let alloyed_asset_to_burn = coin( in_amount.u128(), self.alloyed_asset.get_alloyed_denom(deps.storage)?, ) @@ -514,7 +514,7 @@ impl Transmuter<'_> { token_out.amount, self.alloyed_asset.get_normalization_factor(deps.storage)?, )?; - let token_in = Coin::new(token_in_amount.u128(), token_in_denom); + let token_in = coin(token_in_amount.u128(), token_in_denom); pool.join_pool(&[token_in.clone()])?; (pool, token_in) } @@ -528,7 +528,7 @@ impl Transmuter<'_> { self.alloyed_asset.get_normalization_factor(deps.storage)?, vec![(token_out.clone(), token_out_norm_factor)], )?; - let token_in = Coin::new(token_in_amount.u128(), token_in_denom); + let token_in = coin(token_in_amount.u128(), token_in_denom); pool.exit_pool(&[token_out])?; (pool, token_in) } @@ -574,7 +574,7 @@ impl Transmuter<'_> { Uint128::zero(), self.alloyed_asset.get_normalization_factor(deps.storage)?, )?; - let token_out = Coin::new(token_out_amount.u128(), token_out_denom); + let token_out = coin(token_out_amount.u128(), token_out_denom); pool.join_pool(&[token_in])?; (pool, token_out) } @@ -589,7 +589,7 @@ impl Transmuter<'_> { token_out_norm_factor, Uint128::zero(), )?; - let token_out = Coin::new(token_out_amount.u128(), token_out_denom); + let token_out = coin(token_out_amount.u128(), token_out_denom); pool.exit_pool(&[token_out.clone()])?; (pool, token_out) } @@ -826,7 +826,7 @@ mod tests { #[case] res: Result, ) { let mut deps = cosmwasm_std::testing::mock_dependencies(); - let transmuter = Transmuter::default(); + let transmuter = Transmuter::new(); transmuter .alloyed_asset .set_alloyed_denom(&mut deps.storage, &"alloyed".to_string()) @@ -839,21 +839,21 @@ mod tests { #[case( Entrypoint::Exec, SwapToAlloyedConstraint::ExactIn { - tokens_in: &[Coin::new(100, "denom1")], + tokens_in: &[coin(100, "denom1")], token_out_min_amount: Uint128::one(), }, Addr::unchecked("addr1"), Ok(Response::new() .add_message(MsgMint { sender: MOCK_CONTRACT_ADDR.to_string(), - amount: Some(Coin::new(10000u128, "alloyed").into()), + amount: Some(coin(10000u128, "alloyed").into()), mint_to_address: "addr1".to_string() })), )] #[case( Entrypoint::Sudo, SwapToAlloyedConstraint::ExactIn { - tokens_in: &[Coin::new(100, "denom1")], + tokens_in: &[coin(100, "denom1")], token_out_min_amount: Uint128::one(), }, Addr::unchecked("addr1"), @@ -863,7 +863,7 @@ mod tests { }).unwrap()) .add_message(MsgMint { sender: MOCK_CONTRACT_ADDR.to_string(), - amount: Some(Coin::new(10000u128, "alloyed").into()), + amount: Some(coin(10000u128, "alloyed").into()), mint_to_address: "addr1".to_string() })), )] @@ -878,7 +878,7 @@ mod tests { Ok(Response::new() .add_message(MsgMint { sender: MOCK_CONTRACT_ADDR.to_string(), - amount: Some(Coin::new(10000u128, "alloyed").into()), + amount: Some(coin(10000u128, "alloyed").into()), mint_to_address: "addr1".to_string() })), )] @@ -896,7 +896,7 @@ mod tests { }).unwrap()) .add_message(MsgMint { sender: MOCK_CONTRACT_ADDR.to_string(), - amount: Some(Coin::new(10000u128, "alloyed").into()), + amount: Some(coin(10000u128, "alloyed").into()), mint_to_address: "addr1".to_string() })), )] @@ -907,7 +907,7 @@ mod tests { #[case] expected_res: Result, ) { let mut deps = mock_dependencies(); - let transmuter = Transmuter::default(); + let transmuter = Transmuter::new(); transmuter .alloyed_asset .set_alloyed_denom(&mut deps.storage, &"alloyed".to_string()) @@ -956,12 +956,12 @@ mod tests { Ok(Response::new() .add_message(MsgBurn { sender: MOCK_CONTRACT_ADDR.to_string(), - amount: Some(Coin::new(100u128, "alloyed").into()), + amount: Some(coin(100u128, "alloyed").into()), burn_from_address: "addr1".to_string() }) .add_message(BankMsg::Send { to_address: "addr1".to_string(), - amount: vec![Coin::new(1u128, "denom1")] + amount: vec![coin(1u128, "denom1")] })) )] #[case( @@ -976,12 +976,12 @@ mod tests { Ok(Response::new() .add_message(MsgBurn { sender: MOCK_CONTRACT_ADDR.to_string(), - amount: Some(Coin::new(100u128, "alloyed").into()), + amount: Some(coin(100u128, "alloyed").into()), burn_from_address: MOCK_CONTRACT_ADDR.to_string() }) .add_message(BankMsg::Send { to_address: "addr1".to_string(), - amount: vec![Coin::new(1u128, "denom1")] + amount: vec![coin(1u128, "denom1")] }) .set_data(to_json_binary(&SwapExactAmountInResponseData { token_out_amount: Uint128::from(1u128) @@ -990,7 +990,7 @@ mod tests { #[case( Entrypoint::Exec, SwapFromAlloyedConstraint::ExactOut { - tokens_out: &[Coin::new(1u128, "denom1")], + tokens_out: &[coin(1u128, "denom1")], token_in_max_amount: Uint128::from(100u128), }, BurnTarget::SenderAccount, @@ -998,18 +998,18 @@ mod tests { Ok(Response::new() .add_message(MsgBurn { sender: MOCK_CONTRACT_ADDR.to_string(), - amount: Some(Coin::new(100u128, "alloyed").into()), + amount: Some(coin(100u128, "alloyed").into()), burn_from_address: "addr1".to_string() }) .add_message(BankMsg::Send { to_address: "addr1".to_string(), - amount: vec![Coin::new(1u128, "denom1")] + amount: vec![coin(1u128, "denom1")] })) )] #[case( Entrypoint::Sudo, SwapFromAlloyedConstraint::ExactOut { - tokens_out: &[Coin::new(1u128, "denom1")], + tokens_out: &[coin(1u128, "denom1")], token_in_max_amount: Uint128::from(100u128), }, BurnTarget::SentFunds, @@ -1017,12 +1017,12 @@ mod tests { Ok(Response::new() .add_message(MsgBurn { sender: MOCK_CONTRACT_ADDR.to_string(), - amount: Some(Coin::new(100u128, "alloyed").into()), + amount: Some(coin(100u128, "alloyed").into()), burn_from_address: MOCK_CONTRACT_ADDR.to_string() }) .add_message(BankMsg::Send { to_address: "addr1".to_string(), - amount: vec![Coin::new(1u128, "denom1")] + amount: vec![coin(1u128, "denom1")] }) .set_data(to_json_binary(&SwapExactAmountOutResponseData { token_in_amount: Uint128::from(100u128) @@ -1042,10 +1042,10 @@ mod tests { let mut deps = cosmwasm_std::testing::mock_dependencies_with_balances(&[( alloyed_holder.as_str(), - &[Coin::new(110000000000000u128, "alloyed")], + &[coin(110000000000000u128, "alloyed")], )]); - let transmuter = Transmuter::default(); + let transmuter = Transmuter::new(); transmuter .alloyed_asset .set_alloyed_denom(&mut deps.storage, &"alloyed".to_string()) @@ -1092,7 +1092,7 @@ mod tests { #[case( Entrypoint::Sudo, SwapFromAlloyedConstraint::ExactOut { - tokens_out: &[Coin::new(1000000000000u128, "denom1")], + tokens_out: &[coin(1000000000000u128, "denom1")], token_in_max_amount: Uint128::from(100000000000000u128), }, vec!["denom1"], @@ -1102,12 +1102,12 @@ mod tests { Ok(Response::new() .add_message(MsgBurn { sender: MOCK_CONTRACT_ADDR.to_string(), - amount: Some(Coin::new(100000000000000u128, "alloyed").into()), + amount: Some(coin(100000000000000u128, "alloyed").into()), burn_from_address: MOCK_CONTRACT_ADDR.to_string() }) .add_message(BankMsg::Send { to_address: "addr1".to_string(), - amount: vec![Coin::new(1000000000000u128, "denom1")] + amount: vec![coin(1000000000000u128, "denom1")] }) .set_data(to_json_binary(&SwapExactAmountOutResponseData { token_in_amount: Uint128::from(100000000000000u128) @@ -1127,12 +1127,12 @@ mod tests { Ok(Response::new() .add_message(MsgBurn { sender: MOCK_CONTRACT_ADDR.to_string(), - amount: Some(Coin::new(100000000000000u128, "alloyed").into()), + amount: Some(coin(100000000000000u128, "alloyed").into()), burn_from_address: MOCK_CONTRACT_ADDR.to_string() }) .add_message(BankMsg::Send { to_address: "addr1".to_string(), - amount: vec![Coin::new(1000000000000u128, "denom1")] + amount: vec![coin(1000000000000u128, "denom1")] }) .set_data(to_json_binary(&SwapExactAmountInResponseData { token_out_amount: 1000000000000u128.into(), @@ -1152,18 +1152,18 @@ mod tests { Ok(Response::new() .add_message(MsgBurn { sender: MOCK_CONTRACT_ADDR.to_string(), - amount: Some(Coin::new(100000000000000u128, "alloyed").into()), + amount: Some(coin(100000000000000u128, "alloyed").into()), burn_from_address: "addr1".to_string() }) .add_message(BankMsg::Send { to_address: "addr1".to_string(), - amount: vec![Coin::new(1000000000000u128, "denom1")] + amount: vec![coin(1000000000000u128, "denom1")] })) )] #[case( Entrypoint::Exec, SwapFromAlloyedConstraint::ExactOut { - tokens_out: &[Coin::new(1000000000000u128, "denom1")], + tokens_out: &[coin(1000000000000u128, "denom1")], token_in_max_amount: Uint128::from(100000000000000u128), }, vec!["denom1"], @@ -1173,18 +1173,18 @@ mod tests { Ok(Response::new() .add_message(MsgBurn { sender: MOCK_CONTRACT_ADDR.to_string(), - amount: Some(Coin::new(100000000000000u128, "alloyed").into()), + amount: Some(coin(100000000000000u128, "alloyed").into()), burn_from_address: "addr1".to_string() }) .add_message(BankMsg::Send { to_address: "addr1".to_string(), - amount: vec![Coin::new(1000000000000u128, "denom1")] + amount: vec![coin(1000000000000u128, "denom1")] })) )] #[case( Entrypoint::Sudo, SwapFromAlloyedConstraint::ExactOut { - tokens_out: &[Coin::new(1000000000000u128, "denom1"), Coin::new(1000000000000u128, "denom2")], + tokens_out: &[coin(1000000000000u128, "denom1"), coin(1000000000000u128, "denom2")], token_in_max_amount: Uint128::from(110000000000000u128), }, vec!["denom1", "denom2"], @@ -1194,12 +1194,12 @@ mod tests { Ok(Response::new() .add_message(MsgBurn { sender: MOCK_CONTRACT_ADDR.to_string(), - amount: Some(Coin::new(110000000000000u128, "alloyed").into()), + amount: Some(coin(110000000000000u128, "alloyed").into()), burn_from_address: MOCK_CONTRACT_ADDR.to_string() }) .add_message(BankMsg::Send { to_address: "addr1".to_string(), - amount: vec![Coin::new(1000000000000u128, "denom1"), Coin::new(1000000000000u128, "denom2")] + amount: vec![coin(1000000000000u128, "denom1"), coin(1000000000000u128, "denom2")] }) .set_data(to_json_binary(&SwapExactAmountOutResponseData { token_in_amount: Uint128::from(110000000000000u128), @@ -1208,7 +1208,7 @@ mod tests { #[case( Entrypoint::Sudo, SwapFromAlloyedConstraint::ExactOut { - tokens_out: &[Coin::new(1000000000000u128, "denom1"), Coin::new(500000000000u128, "denom2")], + tokens_out: &[coin(1000000000000u128, "denom1"), coin(500000000000u128, "denom2")], token_in_max_amount: Uint128::from(105000000000000u128), }, vec!["denom1", "denom2"], @@ -1218,12 +1218,12 @@ mod tests { Ok(Response::new() .add_message(MsgBurn { sender: MOCK_CONTRACT_ADDR.to_string(), - amount: Some(Coin::new(105000000000000u128, "alloyed").into()), + amount: Some(coin(105000000000000u128, "alloyed").into()), burn_from_address: MOCK_CONTRACT_ADDR.to_string() }) .add_message(BankMsg::Send { to_address: "addr1".to_string(), - amount: vec![Coin::new(1000000000000u128, "denom1"), Coin::new(500000000000u128, "denom2")], + amount: vec![coin(1000000000000u128, "denom1"), coin(500000000000u128, "denom2")], }) .set_data(to_json_binary(&SwapExactAmountOutResponseData { token_in_amount: Uint128::from(105000000000000u128), @@ -1245,10 +1245,10 @@ mod tests { let mut deps = cosmwasm_std::testing::mock_dependencies_with_balances(&[( alloyed_holder.as_str(), - &[Coin::new(210000000000000u128, "alloyed")], + &[coin(210000000000000u128, "alloyed")], )]); - let transmuter = Transmuter::default(); + let transmuter = Transmuter::new(); transmuter .alloyed_asset .set_alloyed_denom(&mut deps.storage, &"alloyed".to_string()) @@ -1351,7 +1351,7 @@ mod tests { #[test] fn test_swap_non_alloyed_exact_amount_in_with_corrupted_assets() { let mut deps = mock_dependencies(); - let transmuter = Transmuter::default(); + let transmuter = Transmuter::new(); transmuter .alloyed_asset .set_alloyed_denom(&mut deps.storage, &"alloyed".to_string()) @@ -1436,7 +1436,7 @@ mod tests { #[test] fn test_swap_non_alloyed_exact_amount_out_with_corrupted_assets() { let mut deps = mock_dependencies(); - let transmuter = Transmuter::default(); + let transmuter = Transmuter::new(); transmuter .alloyed_asset .set_alloyed_denom(&mut deps.storage, &"alloyed".to_string()) @@ -1520,35 +1520,35 @@ mod tests { #[rstest] #[case( - Coin::new(100u128, "denom1"), + coin(100u128, "denom1"), "denom2", 1000u128, Addr::unchecked("addr1"), Ok(Response::new() .add_message(BankMsg::Send { to_address: "addr1".to_string(), - amount: vec![Coin::new(1000u128, "denom2")] + amount: vec![coin(1000u128, "denom2")] }) .set_data(to_json_binary(&SwapExactAmountInResponseData { token_out_amount: Uint128::from(1000u128) }).unwrap())) )] #[case( - Coin::new(100u128, "denom2"), + coin(100u128, "denom2"), "denom1", 10u128, Addr::unchecked("addr1"), Ok(Response::new() .add_message(BankMsg::Send { to_address: "addr1".to_string(), - amount: vec![Coin::new(10u128, "denom1")] + amount: vec![coin(10u128, "denom1")] }) .set_data(to_json_binary(&SwapExactAmountInResponseData { token_out_amount: Uint128::from(10u128) }).unwrap())) )] #[case( - Coin::new(100u128, "denom2"), + coin(100u128, "denom2"), "denom1", 100u128, Addr::unchecked("addr1"), @@ -1558,13 +1558,13 @@ mod tests { }) )] #[case( - Coin::new(100000000001u128, "denom1"), + coin(100000000001u128, "denom1"), "denom2", 1000000000010u128, Addr::unchecked("addr1"), Err(ContractError::InsufficientPoolAsset { - required: Coin::new(1000000000010u128, "denom2"), - available: Coin::new(1000000000000u128, "denom2"), + required: coin(1000000000010u128, "denom2"), + available: coin(1000000000000u128, "denom2"), }) )] fn test_swap_non_alloyed_exact_amount_in( @@ -1576,10 +1576,10 @@ mod tests { ) { let mut deps = cosmwasm_std::testing::mock_dependencies_with_balances(&[( sender.to_string().as_str(), - &[Coin::new(2000000000000u128, "alloyed")], + &[coin(2000000000000u128, "alloyed")], )]); - let transmuter = Transmuter::default(); + let transmuter = Transmuter::new(); transmuter .alloyed_asset .set_alloyed_denom(&mut deps.storage, &"alloyed".to_string()) @@ -1620,12 +1620,12 @@ mod tests { #[case( "denom1", 100u128, - Coin::new(1000u128, "denom2"), + coin(1000u128, "denom2"), Addr::unchecked("addr1"), Ok(Response::new() .add_message(BankMsg::Send { to_address: "addr1".to_string(), - amount: vec![Coin::new(1000u128, "denom2")] + amount: vec![coin(1000u128, "denom2")] }) .set_data(to_json_binary(&SwapExactAmountOutResponseData { token_in_amount: 100u128.into() @@ -1634,12 +1634,12 @@ mod tests { #[case( "denom2", 100u128, - Coin::new(10u128, "denom1"), + coin(10u128, "denom1"), Addr::unchecked("addr1"), Ok(Response::new() .add_message(BankMsg::Send { to_address: "addr1".to_string(), - amount: vec![Coin::new(10u128, "denom1")] + amount: vec![coin(10u128, "denom1")] }) .set_data(to_json_binary(&SwapExactAmountOutResponseData { token_in_amount: 100u128.into() @@ -1648,7 +1648,7 @@ mod tests { #[case( "denom2", 100u128, - Coin::new(100u128, "denom1"), + coin(100u128, "denom1"), Addr::unchecked("addr1"), Err(ContractError::ExcessiveRequiredTokenIn { limit: 100u128.into(), @@ -1658,11 +1658,11 @@ mod tests { #[case( "denom1", 100000000001u128, - Coin::new(1000000000010u128, "denom2"), + coin(1000000000010u128, "denom2"), Addr::unchecked("addr1"), Err(ContractError::InsufficientPoolAsset { - required: Coin::new(1000000000010u128, "denom2"), - available: Coin::new(1000000000000u128, "denom2"), + required: coin(1000000000010u128, "denom2"), + available: coin(1000000000000u128, "denom2"), }) )] fn test_swap_non_alloyed_exact_amount_out( @@ -1674,10 +1674,10 @@ mod tests { ) { let mut deps = cosmwasm_std::testing::mock_dependencies_with_balances(&[( sender.to_string().as_str(), - &[Coin::new(2000000000000u128, "alloyed")], + &[coin(2000000000000u128, "alloyed")], )]); - let transmuter = Transmuter::default(); + let transmuter = Transmuter::new(); transmuter .alloyed_asset .set_alloyed_denom(&mut deps.storage, &"alloyed".to_string()) @@ -1799,10 +1799,10 @@ mod tests { let sender = Addr::unchecked("addr1"); let mut deps = cosmwasm_std::testing::mock_dependencies_with_balances(&[( sender.to_string().as_str(), - &[Coin::new(2000000000000u128, "alloyed")], + &[coin(2000000000000u128, "alloyed")], )]); - let transmuter = Transmuter::default(); + let transmuter = Transmuter::new(); transmuter .alloyed_asset .set_alloyed_denom(&mut deps.storage, &"alloyed".to_string()) @@ -1848,7 +1848,7 @@ mod tests { pool = transmuter.pool.load(&deps.storage).unwrap(); assert_eq!(pool, init_pool); - pool.exit_pool(&[Coin::new(1000000000000u128, "denom2")]) + pool.exit_pool(&[coin(1000000000000u128, "denom2")]) .unwrap(); transmuter.pool.save(&mut deps.storage, &pool).unwrap(); @@ -1876,7 +1876,7 @@ mod tests { // Save the updated pool transmuter.pool.save(&mut deps.storage, &pool).unwrap(); - pool.exit_pool(&[Coin::new(1000000000000u128, "denom3")]) + pool.exit_pool(&[coin(1000000000000u128, "denom3")]) .unwrap(); let res = transmuter.clean_up_drained_corrupted_assets(&mut deps.storage, &mut pool); @@ -1898,7 +1898,7 @@ mod tests { #[test] fn test_clean_up_drained_corrupted_assets_group_not_corrupted() { let mut deps = mock_dependencies(); - let transmuter = Transmuter::default(); + let transmuter = Transmuter::new(); // Initialize the pool with non-corrupted assets and groups let init_pool = TransmuterPool { @@ -1932,7 +1932,7 @@ mod tests { assert_eq!(pool, init_pool); // Drain denom2 from the pool - pool.exit_pool(&[Coin::new(1000000000000u128, "denom2")]) + pool.exit_pool(&[coin(1000000000000u128, "denom2")]) .unwrap(); transmuter.pool.save(&mut deps.storage, &pool).unwrap(); @@ -1961,7 +1961,7 @@ mod tests { transmuter.pool.save(&mut deps.storage, &pool).unwrap(); // Drain denom3 from the pool - pool.exit_pool(&[Coin::new(1000000000000u128, "denom3")]) + pool.exit_pool(&[coin(1000000000000u128, "denom3")]) .unwrap(); let res = transmuter.clean_up_drained_corrupted_assets(&mut deps.storage, &mut pool); diff --git a/contracts/transmuter/src/test/cases/scenarios.rs b/contracts/transmuter/src/test/cases/scenarios.rs index 584dba5..01593ca 100644 --- a/contracts/transmuter/src/test/cases/scenarios.rs +++ b/contracts/transmuter/src/test/cases/scenarios.rs @@ -15,7 +15,7 @@ use crate::{ }, ContractError, }; -use cosmwasm_std::{Coin, Decimal, Uint128, Uint64}; +use cosmwasm_std::{coin, Decimal, Uint128, Uint64}; use osmosis_std::types::{ cosmos::bank::v1beta1::MsgSend, @@ -40,14 +40,14 @@ fn test_join_pool() { .with_account( "provider_1", vec![ - Coin::new(2_000, COSMOS_USDC), - Coin::new(2_000, AXL_USDC), - Coin::new(2_000, "urandom"), + coin(2_000, COSMOS_USDC), + coin(2_000, AXL_USDC), + coin(2_000, "urandom"), ], ) .with_account( "provider_2", - vec![Coin::new(2_000, COSMOS_USDC), Coin::new(2_000, AXL_USDC)], + vec![coin(2_000, COSMOS_USDC), coin(2_000, AXL_USDC)], ) .with_account("moderator", vec![]) .with_instantiate_msg(InstantiateMsg { @@ -71,7 +71,7 @@ fn test_join_pool() { assert_contract_err(ContractError::AtLeastSingleTokenExpected {}, err); // fail to join pool with denom that is not in the pool - let tokens_in = vec![Coin::new(1_000, "urandom")]; + let tokens_in = vec![coin(1_000, "urandom")]; let err = t .contract .execute(&ExecMsg::JoinPool {}, &tokens_in, &t.accounts["provider_1"]) @@ -86,7 +86,7 @@ fn test_join_pool() { ); // join pool with correct pool's denom should added to the contract's balance and update state - let tokens_in = vec![Coin::new(1_000, COSMOS_USDC)]; + let tokens_in = vec![coin(1_000, COSMOS_USDC)]; t.contract .execute(&ExecMsg::JoinPool {}, &tokens_in, &t.accounts["provider_1"]) @@ -105,7 +105,7 @@ fn test_join_pool() { assert_eq!( total_pool_liquidity, - vec![Coin::new(0, AXL_USDC), tokens_in[0].clone()] + vec![coin(0, AXL_USDC), tokens_in[0].clone()] ); // check shares @@ -125,13 +125,13 @@ fn test_join_pool() { assert_eq!(total_shares, tokens_in[0].amount); // join pool with multiple correct pool's denom should added to the contract's balance and update state - let tokens_in = vec![Coin::new(1_000, AXL_USDC), Coin::new(1_000, COSMOS_USDC)]; + let tokens_in = vec![coin(1_000, AXL_USDC), coin(1_000, COSMOS_USDC)]; t.contract .execute(&ExecMsg::JoinPool {}, &tokens_in, &t.accounts["provider_1"]) .unwrap(); // check contract balances - t.assert_contract_balances(&[Coin::new(1_000, AXL_USDC), Coin::new(2_000, COSMOS_USDC)]); + t.assert_contract_balances(&[coin(1_000, AXL_USDC), coin(2_000, COSMOS_USDC)]); // check pool balance let GetTotalPoolLiquidityResponse { @@ -143,7 +143,7 @@ fn test_join_pool() { assert_eq!( total_pool_liquidity, - vec![Coin::new(1_000, AXL_USDC), Coin::new(2_000, COSMOS_USDC)] + vec![coin(1_000, AXL_USDC), coin(2_000, COSMOS_USDC)] ); // check shares @@ -163,13 +163,13 @@ fn test_join_pool() { assert_eq!(total_shares, Uint128::new(3000)); // join pool with another provider with multiple correct pool's denom should added to the contract's balance and update state - let tokens_in = vec![Coin::new(2_000, AXL_USDC), Coin::new(2_000, COSMOS_USDC)]; + let tokens_in = vec![coin(2_000, AXL_USDC), coin(2_000, COSMOS_USDC)]; t.contract .execute(&ExecMsg::JoinPool {}, &tokens_in, &t.accounts["provider_2"]) .unwrap(); // check contract balances - t.assert_contract_balances(&[Coin::new(3_000, AXL_USDC), Coin::new(4_000, COSMOS_USDC)]); + t.assert_contract_balances(&[coin(3_000, AXL_USDC), coin(4_000, COSMOS_USDC)]); // check pool balance let GetTotalPoolLiquidityResponse { @@ -181,7 +181,7 @@ fn test_join_pool() { assert_eq!( total_pool_liquidity, - vec![Coin::new(3_000, AXL_USDC), Coin::new(4_000, COSMOS_USDC)] + vec![coin(3_000, AXL_USDC), coin(4_000, COSMOS_USDC)] ); // check shares @@ -211,13 +211,13 @@ fn test_swap() { .with_account( "alice", vec![ - Coin::new(1_500, AXL_USDC), - Coin::new(1_000, COSMOS_USDC), - Coin::new(1_000, "urandom2"), + coin(1_500, AXL_USDC), + coin(1_000, COSMOS_USDC), + coin(1_000, "urandom2"), ], ) - .with_account("bob", vec![Coin::new(29_902, AXL_USDC)]) - .with_account("provider", vec![Coin::new(200_000, COSMOS_USDC)]) + .with_account("bob", vec![coin(29_902, AXL_USDC)]) + .with_account("provider", vec![coin(200_000, COSMOS_USDC)]) .with_instantiate_msg(InstantiateMsg { pool_asset_configs: vec![ AssetConfig::from_denom_str(AXL_USDC), @@ -231,7 +231,7 @@ fn test_swap() { .build(&app); // join pool - let tokens_in = vec![Coin::new(100_000, COSMOS_USDC)]; + let tokens_in = vec![coin(100_000, COSMOS_USDC)]; // join pool with send tokens should not update pool balance bank.send( @@ -248,7 +248,7 @@ fn test_swap() { .swap_exact_amount_in( MsgSwapExactAmountIn { sender: t.accounts["alice"].address(), - token_in: Some(Coin::new(1_500, AXL_USDC).into()), + token_in: Some(coin(1_500, AXL_USDC).into()), routes: vec![SwapAmountInRoute { pool_id: t.contract.pool_id, token_out_denom: COSMOS_USDC.to_string(), @@ -261,8 +261,8 @@ fn test_swap() { assert_contract_err( ContractError::InsufficientPoolAsset { - required: Coin::new(1_500, COSMOS_USDC), - available: Coin::new(0, COSMOS_USDC), + required: coin(1_500, COSMOS_USDC), + available: coin(0, COSMOS_USDC), }, err, ); @@ -277,7 +277,7 @@ fn test_swap() { .swap_exact_amount_in( MsgSwapExactAmountIn { sender: t.accounts["alice"].address(), - token_in: Some(Coin::new(1_000, COSMOS_USDC).into()), + token_in: Some(coin(1_000, COSMOS_USDC).into()), routes: vec![SwapAmountInRoute { pool_id: t.contract.pool_id, token_out_denom: "urandom".to_string(), @@ -300,7 +300,7 @@ fn test_swap() { .swap_exact_amount_in( MsgSwapExactAmountIn { sender: t.accounts["alice"].address(), - token_in: Some(Coin::new(1_000, COSMOS_USDC).into()), + token_in: Some(coin(1_000, COSMOS_USDC).into()), routes: vec![SwapAmountInRoute { pool_id: t.contract.pool_id, token_out_denom: "urandom2".to_string(), @@ -333,6 +333,7 @@ fn test_swap() { pool_id: t.contract.pool_id, token_in: format!("1500{AXL_USDC}"), routes: routes.clone(), + sender: t.accounts["alice"].address(), }, ) .unwrap(); @@ -340,7 +341,7 @@ fn test_swap() { assert_eq!(token_out_amount, "1500"); // swap with correct token_in should succeed this time - let token_in = Coin::new(1_500, AXL_USDC); + let token_in = coin(1_500, AXL_USDC); cp.swap_exact_amount_in( MsgSwapExactAmountIn { sender: t.accounts["alice"].address(), @@ -354,8 +355,8 @@ fn test_swap() { // check balances t.assert_contract_balances(&[ - Coin::new(1_500, AXL_USDC), - Coin::new(100_000 + 100_000 - 1_500, COSMOS_USDC), // +100_000 due to bank send + coin(1_500, AXL_USDC), + coin(100_000 + 100_000 - 1_500, COSMOS_USDC), // +100_000 due to bank send ]); let GetTotalPoolLiquidityResponse { @@ -367,25 +368,19 @@ fn test_swap() { assert_eq!( total_pool_liquidity, - vec![ - Coin::new(1_500, AXL_USDC), - Coin::new(100_000 - 1_500, COSMOS_USDC) - ] + vec![coin(1_500, AXL_USDC), coin(100_000 - 1_500, COSMOS_USDC)] ); // +1_000 due to existing alice balance t.assert_account_balances( "alice", - vec![ - Coin::new(1_500 + 1_000, COSMOS_USDC), - Coin::new(1_000, "urandom2"), - ], + vec![coin(1_500 + 1_000, COSMOS_USDC), coin(1_000, "urandom2")], vec!["uosmo"], ); // swap again with another user // swap with correct token_in should succeed this time - let token_in = Coin::new(29_902, AXL_USDC); + let token_in = coin(29_902, AXL_USDC); cp.swap_exact_amount_in( MsgSwapExactAmountIn { sender: t.accounts["bob"].address(), @@ -402,8 +397,8 @@ fn test_swap() { // check balances t.assert_contract_balances(&[ - Coin::new(1_500 + 29_902, AXL_USDC), - Coin::new(100_000 + 100_000 - 1_500 - 29_902, COSMOS_USDC), // +100_000 due to bank send + coin(1_500 + 29_902, AXL_USDC), + coin(100_000 + 100_000 - 1_500 - 29_902, COSMOS_USDC), // +100_000 due to bank send ]); let GetTotalPoolLiquidityResponse { @@ -416,15 +411,15 @@ fn test_swap() { assert_eq!( total_pool_liquidity, vec![ - Coin::new(1_500 + 29_902, AXL_USDC), - Coin::new(100_000 - 1_500 - 29_902, COSMOS_USDC) + coin(1_500 + 29_902, AXL_USDC), + coin(100_000 - 1_500 - 29_902, COSMOS_USDC) ] ); - t.assert_account_balances("bob", vec![Coin::new(29_902, COSMOS_USDC)], vec!["uosmo"]); + t.assert_account_balances("bob", vec![coin(29_902, COSMOS_USDC)], vec!["uosmo"]); // swap back with `SwapExactAmountOut` - let token_out = Coin::new(1_500, AXL_USDC); + let token_out = coin(1_500, AXL_USDC); cp.swap_exact_amount_out( MsgSwapExactAmountOut { @@ -449,23 +444,20 @@ fn test_swap() { assert_eq!( total_pool_liquidity, - vec![ - Coin::new(29_902, AXL_USDC), - Coin::new(100_000 - 29_902, COSMOS_USDC), - ] + vec![coin(29_902, AXL_USDC), coin(100_000 - 29_902, COSMOS_USDC),] ); // check balances t.assert_contract_balances(&[ - Coin::new(29_902, AXL_USDC), - Coin::new(100_000 + 100_000 - 29_902, COSMOS_USDC), + coin(29_902, AXL_USDC), + coin(100_000 + 100_000 - 29_902, COSMOS_USDC), ]); t.assert_account_balances( "bob", vec![ - Coin::new(1_500, AXL_USDC), - Coin::new(29_902 - 1_500, COSMOS_USDC), // +100_000 due to bank send + coin(1_500, AXL_USDC), + coin(29_902 - 1_500, COSMOS_USDC), // +100_000 due to bank send ], vec!["uosmo"], ); @@ -477,9 +469,9 @@ fn test_exit_pool() { let cp = CosmwasmPool::new(&app); let t = TestEnvBuilder::new() - .with_account("user", vec![Coin::new(1_500, AXL_USDC)]) - .with_account("provider_1", vec![Coin::new(100_000, COSMOS_USDC)]) - .with_account("provider_2", vec![Coin::new(100_000, COSMOS_USDC)]) + .with_account("user", vec![coin(1_500, AXL_USDC)]) + .with_account("provider_1", vec![coin(100_000, COSMOS_USDC)]) + .with_account("provider_2", vec![coin(100_000, COSMOS_USDC)]) .with_instantiate_msg(InstantiateMsg { pool_asset_configs: vec![ AssetConfig::from_denom_str(AXL_USDC), @@ -496,7 +488,7 @@ fn test_exit_pool() { t.contract .execute( &ExecMsg::JoinPool {}, - &[Coin::new(100_000, COSMOS_USDC)], + &[coin(100_000, COSMOS_USDC)], &t.accounts["provider_1"], ) .unwrap(); @@ -504,13 +496,13 @@ fn test_exit_pool() { t.contract .execute( &ExecMsg::JoinPool {}, - &[Coin::new(100_000, COSMOS_USDC)], + &[coin(100_000, COSMOS_USDC)], &t.accounts["provider_2"], ) .unwrap(); // swap to build up token_in - let token_in = Coin::new(1_500, AXL_USDC); + let token_in = coin(1_500, AXL_USDC); cp.swap_exact_amount_in( MsgSwapExactAmountIn { @@ -531,7 +523,7 @@ fn test_exit_pool() { .contract .execute( &ExecMsg::ExitPool { - tokens_out: vec![Coin::new(1_500, AXL_USDC)], + tokens_out: vec![coin(1_500, AXL_USDC)], }, &[], &t.accounts["user"], @@ -550,7 +542,7 @@ fn test_exit_pool() { t.contract .execute( &ExecMsg::ExitPool { - tokens_out: vec![Coin::new(500, AXL_USDC)], + tokens_out: vec![coin(500, AXL_USDC)], }, &[], &t.accounts["provider_1"], @@ -575,8 +567,8 @@ fn test_exit_pool() { // check balances t.assert_contract_balances(&[ - Coin::new(1500 - 500, AXL_USDC), - Coin::new(200_000 - 1500, COSMOS_USDC), + coin(1500 - 500, AXL_USDC), + coin(200_000 - 1500, COSMOS_USDC), ]); let GetTotalPoolLiquidityResponse { @@ -589,8 +581,8 @@ fn test_exit_pool() { assert_eq!( total_pool_liquidity, vec![ - Coin::new(1500 - 500, AXL_USDC), - Coin::new(200_000 - 1500, COSMOS_USDC) + coin(1500 - 500, AXL_USDC), + coin(200_000 - 1500, COSMOS_USDC) ] ); @@ -598,7 +590,7 @@ fn test_exit_pool() { t.contract .execute( &ExecMsg::ExitPool { - tokens_out: vec![Coin::new(1_000, AXL_USDC), Coin::new(99_000, COSMOS_USDC)], + tokens_out: vec![coin(1_000, AXL_USDC), coin(99_000, COSMOS_USDC)], }, &[], &t.accounts["provider_2"], @@ -622,7 +614,7 @@ fn test_exit_pool() { assert_eq!(total_shares, Uint128::new(200_000 - 500 - 1000 - 99_000)); // check balances - t.assert_contract_balances(&[Coin::new(200_000 - 1500 - 99_000, COSMOS_USDC)]); + t.assert_contract_balances(&[coin(200_000 - 1500 - 99_000, COSMOS_USDC)]); let GetTotalPoolLiquidityResponse { total_pool_liquidity, @@ -634,8 +626,8 @@ fn test_exit_pool() { assert_eq!( total_pool_liquidity, vec![ - Coin::new(0, AXL_USDC), - Coin::new(200_000 - 1500 - 99_000, COSMOS_USDC) + coin(0, AXL_USDC), + coin(200_000 - 1500 - 99_000, COSMOS_USDC) ] ); @@ -644,7 +636,7 @@ fn test_exit_pool() { .contract .execute( &ExecMsg::ExitPool { - tokens_out: vec![Coin::new(1, AXL_USDC)], + tokens_out: vec![coin(1, AXL_USDC)], }, &[], &t.accounts["provider_2"], @@ -664,7 +656,7 @@ fn test_exit_pool() { .contract .execute( &ExecMsg::ExitPool { - tokens_out: vec![Coin::new(1, AXL_USDC)], + tokens_out: vec![coin(1, AXL_USDC)], }, &[], &t.accounts["provider_1"], @@ -673,8 +665,8 @@ fn test_exit_pool() { assert_contract_err( ContractError::InsufficientPoolAsset { - required: Coin::new(1, AXL_USDC), - available: Coin::new(0, AXL_USDC), + required: coin(1, AXL_USDC), + available: coin(0, AXL_USDC), }, err, ); @@ -686,9 +678,9 @@ fn test_3_pool_swap() { let cp = CosmwasmPool::new(&app); let t = TestEnvBuilder::new() - .with_account("alice", vec![Coin::new(1_500, AXL_USDC)]) - .with_account("bob", vec![Coin::new(1_500, AXL_DAI)]) - .with_account("provider", vec![Coin::new(100_000, COSMOS_USDC)]) + .with_account("alice", vec![coin(1_500, AXL_USDC)]) + .with_account("bob", vec![coin(1_500, AXL_DAI)]) + .with_account("provider", vec![coin(100_000, COSMOS_USDC)]) .with_instantiate_msg(InstantiateMsg { pool_asset_configs: vec![ AssetConfig::from_denom_str(AXL_USDC), @@ -710,7 +702,7 @@ fn test_3_pool_swap() { t.contract .execute( &ExecMsg::JoinPool {}, - &[Coin::new(100_000, COSMOS_USDC)], + &[coin(100_000, COSMOS_USDC)], &t.accounts["provider"], ) .unwrap(); @@ -726,7 +718,7 @@ fn test_3_pool_swap() { assert_eq!(shares, Uint128::new(100_000)); // check contract balances - t.assert_contract_balances(&[Coin::new(100_000, COSMOS_USDC)]); + t.assert_contract_balances(&[coin(100_000, COSMOS_USDC)]); // check provider balances t.assert_account_balances("provider", vec![], vec!["uosmo", &share_denom]); @@ -742,9 +734,9 @@ fn test_3_pool_swap() { assert_eq!( total_pool_liquidity, vec![ - Coin::new(0, AXL_USDC), - Coin::new(0, AXL_DAI), - Coin::new(100_000, COSMOS_USDC) + coin(0, AXL_USDC), + coin(0, AXL_DAI), + coin(100_000, COSMOS_USDC) ] ); @@ -754,7 +746,7 @@ fn test_3_pool_swap() { .swap_exact_amount_in( MsgSwapExactAmountIn { sender: t.accounts["alice"].address(), - token_in: Some(Coin::new(1_000, AXL_USDC).into()), + token_in: Some(coin(1_000, AXL_USDC).into()), routes: vec![SwapAmountInRoute { pool_id: t.contract.pool_id, token_out_denom: AXL_DAI.to_string(), @@ -767,8 +759,8 @@ fn test_3_pool_swap() { assert_contract_err( ContractError::InsufficientPoolAsset { - required: Coin::new(1_000, AXL_DAI), - available: Coin::new(0, AXL_DAI), + required: coin(1_000, AXL_DAI), + available: coin(0, AXL_DAI), }, err, ); @@ -777,7 +769,7 @@ fn test_3_pool_swap() { cp.swap_exact_amount_in( MsgSwapExactAmountIn { sender: t.accounts["alice"].address(), - token_in: Some(Coin::new(1_000, AXL_USDC).into()), + token_in: Some(coin(1_000, AXL_USDC).into()), routes: vec![SwapAmountInRoute { pool_id: t.contract.pool_id, token_out_denom: COSMOS_USDC.to_string(), @@ -789,12 +781,12 @@ fn test_3_pool_swap() { .unwrap(); // check contract balances - t.assert_contract_balances(&[Coin::new(1_000, AXL_USDC), Coin::new(99_000, COSMOS_USDC)]); + t.assert_contract_balances(&[coin(1_000, AXL_USDC), coin(99_000, COSMOS_USDC)]); // check alice balance t.assert_account_balances( "alice", - vec![Coin::new(500, AXL_USDC), Coin::new(1_000, COSMOS_USDC)], + vec![coin(500, AXL_USDC), coin(1_000, COSMOS_USDC)], vec!["uosmo", "ucosm"], ); @@ -809,9 +801,9 @@ fn test_3_pool_swap() { assert_eq!( total_pool_liquidity, vec![ - Coin::new(1_000, AXL_USDC), - Coin::new(0, AXL_DAI), - Coin::new(99_000, COSMOS_USDC) + coin(1_000, AXL_USDC), + coin(0, AXL_DAI), + coin(99_000, COSMOS_USDC) ] ); @@ -820,7 +812,7 @@ fn test_3_pool_swap() { cp.swap_exact_amount_in( MsgSwapExactAmountIn { sender: t.accounts["bob"].address(), - token_in: Some(Coin::new(1_000, AXL_DAI).into()), + token_in: Some(coin(1_000, AXL_DAI).into()), routes: vec![SwapAmountInRoute { pool_id: t.contract.pool_id, token_out_denom: AXL_USDC.to_string(), @@ -832,12 +824,12 @@ fn test_3_pool_swap() { .unwrap(); // check contract balances - t.assert_contract_balances(&[Coin::new(1_000, AXL_DAI), Coin::new(99_000, COSMOS_USDC)]); + t.assert_contract_balances(&[coin(1_000, AXL_DAI), coin(99_000, COSMOS_USDC)]); // check bob balances t.assert_account_balances( "bob", - vec![Coin::new(500, AXL_DAI), Coin::new(1_000, AXL_USDC)], + vec![coin(500, AXL_DAI), coin(1_000, AXL_USDC)], vec!["uosmo"], ); @@ -852,9 +844,9 @@ fn test_3_pool_swap() { assert_eq!( total_pool_liquidity, vec![ - Coin::new(0, AXL_USDC), - Coin::new(1_000, AXL_DAI), - Coin::new(99_000, COSMOS_USDC) + coin(0, AXL_USDC), + coin(1_000, AXL_DAI), + coin(99_000, COSMOS_USDC) ] ); @@ -862,7 +854,7 @@ fn test_3_pool_swap() { t.contract .execute( &ExecMsg::ExitPool { - tokens_out: vec![Coin::new(1_000, AXL_DAI), Coin::new(99_000, COSMOS_USDC)], + tokens_out: vec![coin(1_000, AXL_DAI), coin(99_000, COSMOS_USDC)], }, &[], &t.accounts["provider"], @@ -882,17 +874,13 @@ fn test_3_pool_swap() { assert_eq!( total_pool_liquidity, - vec![ - Coin::new(0, AXL_USDC), - Coin::new(0, AXL_DAI), - Coin::new(0, COSMOS_USDC) - ] + vec![coin(0, AXL_USDC), coin(0, AXL_DAI), coin(0, COSMOS_USDC)] ); t.assert_contract_balances(&[]); t.assert_account_balances( "provider", - vec![Coin::new(1_000, AXL_DAI), Coin::new(99_000, COSMOS_USDC)], + vec![coin(1_000, AXL_DAI), coin(99_000, COSMOS_USDC)], vec!["uosmo"], ); } @@ -903,11 +891,11 @@ fn test_swap_alloyed_asset() { let alloyed_asset_subdenom = "eth"; let t = TestEnvBuilder::new() - .with_account("alice", vec![Coin::new(1_500, AXL_ETH)]) - .with_account("bob", vec![Coin::new(1_500, WH_ETH)]) + .with_account("alice", vec![coin(1_500, AXL_ETH)]) + .with_account("bob", vec![coin(1_500, WH_ETH)]) .with_account( "provider", - vec![Coin::new(100_000, AXL_ETH), Coin::new(100_000, WH_ETH)], + vec![coin(100_000, AXL_ETH), coin(100_000, WH_ETH)], ) .with_instantiate_msg(InstantiateMsg { pool_asset_configs: vec![ @@ -939,32 +927,21 @@ fn test_limiters() { let app = OsmosisTestApp::new(); let cp = CosmwasmPool::new(&app); - let admin = app - .init_account(&[Coin::new(100_000u128, "uosmo")]) - .unwrap(); + let admin = app.init_account(&[coin(100_000u128, "uosmo")]).unwrap(); let t = TestEnvBuilder::new() .with_account( "alice", - vec![ - Coin::new(1_000_000, AXL_USDC), - Coin::new(1_000_000, COSMOS_USDC), - ], + vec![coin(1_000_000, AXL_USDC), coin(1_000_000, COSMOS_USDC)], ) .with_account( "bob", - vec![ - Coin::new(1_000_000, AXL_USDC), - Coin::new(1_000_000, COSMOS_USDC), - ], + vec![coin(1_000_000, AXL_USDC), coin(1_000_000, COSMOS_USDC)], ) .with_account("admin", vec![]) .with_account( "provider", - vec![ - Coin::new(1_000_000, AXL_USDC), - Coin::new(1_000_000, COSMOS_USDC), - ], + vec![coin(1_000_000, AXL_USDC), coin(1_000_000, COSMOS_USDC)], ) .with_instantiate_msg(InstantiateMsg { pool_asset_configs: vec![ @@ -1102,10 +1079,7 @@ fn test_limiters() { t.contract .execute( &ExecMsg::JoinPool {}, - &[ - Coin::new(500_000, AXL_USDC), - Coin::new(500_000, COSMOS_USDC), - ], + &[coin(500_000, AXL_USDC), coin(500_000, COSMOS_USDC)], &t.accounts["provider"], ) .unwrap(); @@ -1117,7 +1091,7 @@ fn test_limiters() { .swap_exact_amount_in( MsgSwapExactAmountIn { sender: t.accounts["alice"].address(), - token_in: Some(Coin::new(100_001, AXL_USDC).into()), + token_in: Some(coin(100_001, AXL_USDC).into()), routes: vec![SwapAmountInRoute { pool_id: t.contract.pool_id, token_out_denom: COSMOS_USDC.to_string(), @@ -1142,7 +1116,7 @@ fn test_limiters() { .swap_exact_amount_out( MsgSwapExactAmountOut { sender: t.accounts["alice"].address(), - token_out: Some(Coin::new(50_001, AXL_USDC).into()), + token_out: Some(coin(50_001, AXL_USDC).into()), routes: vec![SwapAmountOutRoute { pool_id: t.contract.pool_id, token_in_denom: COSMOS_USDC.to_string(), @@ -1166,7 +1140,7 @@ fn test_limiters() { cp.swap_exact_amount_in( MsgSwapExactAmountIn { sender: t.accounts["alice"].address(), - token_in: Some(Coin::new(50_000, COSMOS_USDC).into()), + token_in: Some(coin(50_000, COSMOS_USDC).into()), routes: vec![SwapAmountInRoute { pool_id: t.contract.pool_id, token_out_denom: AXL_USDC.to_string(), @@ -1184,7 +1158,7 @@ fn test_limiters() { .contract .execute( &ExecMsg::ExitPool { - tokens_out: vec![Coin::new(200_000, COSMOS_USDC)], + tokens_out: vec![coin(200_000, COSMOS_USDC)], }, &[], &t.accounts["provider"], @@ -1205,7 +1179,7 @@ fn test_limiters() { .contract .execute( &ExecMsg::JoinPool {}, - &[Coin::new(200_000, AXL_USDC)], + &[coin(200_000, AXL_USDC)], &t.accounts["provider"], ) .unwrap_err(); @@ -1213,7 +1187,7 @@ fn test_limiters() { assert_contract_err( ContractError::UpperLimitExceeded { scope: Scope::denom(AXL_USDC), - upper_limit: Decimal::from_str("0.525034626038781163").unwrap(), + upper_limit: Decimal::from_str("0.525017349063150589").unwrap(), value: Decimal::from_str("0.5416666666666666").unwrap(), }, err, @@ -1230,7 +1204,7 @@ fn test_limiters() { .swap_exact_amount_in( MsgSwapExactAmountIn { sender: t.accounts["provider"].address(), - token_in: Some(Coin::new(200_000, alloyed_denom.clone()).into()), + token_in: Some(coin(200_000, alloyed_denom.clone()).into()), routes: vec![SwapAmountInRoute { pool_id: t.contract.pool_id, token_out_denom: AXL_USDC.to_string(), @@ -1255,7 +1229,7 @@ fn test_limiters() { .swap_exact_amount_out( MsgSwapExactAmountOut { sender: t.accounts["provider"].address(), - token_out: Some(Coin::new(200_000, alloyed_denom).into()), + token_out: Some(coin(200_000, alloyed_denom).into()), routes: vec![SwapAmountOutRoute { pool_id: t.contract.pool_id, token_in_denom: COSMOS_USDC.to_string(), @@ -1269,7 +1243,7 @@ fn test_limiters() { assert_contract_err( ContractError::UpperLimitExceeded { scope: Scope::denom(COSMOS_USDC), - upper_limit: Decimal::from_str("0.575").unwrap(), + upper_limit: Decimal::from_str("0.57498265093684941").unwrap(), value: Decimal::from_str("0.625").unwrap(), }, err, @@ -1281,32 +1255,21 @@ fn test_register_limiter_after_having_liquidity() { let app = OsmosisTestApp::new(); let cp = CosmwasmPool::new(&app); - let admin = app - .init_account(&[Coin::new(100_000u128, "uosmo")]) - .unwrap(); + let admin = app.init_account(&[coin(100_000u128, "uosmo")]).unwrap(); let t = TestEnvBuilder::new() .with_account( "alice", - vec![ - Coin::new(1_000_000, AXL_USDC), - Coin::new(1_000_000, COSMOS_USDC), - ], + vec![coin(1_000_000, AXL_USDC), coin(1_000_000, COSMOS_USDC)], ) .with_account( "bob", - vec![ - Coin::new(1_000_000, AXL_USDC), - Coin::new(1_000_000, COSMOS_USDC), - ], + vec![coin(1_000_000, AXL_USDC), coin(1_000_000, COSMOS_USDC)], ) .with_account("admin", vec![]) .with_account( "provider", - vec![ - Coin::new(1_000_000, AXL_USDC), - Coin::new(1_000_000, COSMOS_USDC), - ], + vec![coin(1_000_000, AXL_USDC), coin(1_000_000, COSMOS_USDC)], ) .with_instantiate_msg(InstantiateMsg { pool_asset_configs: vec![ @@ -1329,7 +1292,7 @@ fn test_register_limiter_after_having_liquidity() { cp.swap_exact_amount_in( MsgSwapExactAmountIn { sender: t.accounts["provider"].address(), - token_in: Some(Coin::new(200_000, COSMOS_USDC).into()), + token_in: Some(coin(200_000, COSMOS_USDC).into()), routes: vec![SwapAmountInRoute { pool_id: t.contract.pool_id, token_out_denom: alloyed_denom.clone(), @@ -1358,7 +1321,7 @@ fn test_register_limiter_after_having_liquidity() { .swap_exact_amount_in( MsgSwapExactAmountIn { sender: t.accounts["provider"].address(), - token_in: Some(Coin::new(1, COSMOS_USDC).into()), + token_in: Some(coin(1, COSMOS_USDC).into()), routes: vec![SwapAmountInRoute { pool_id: t.contract.pool_id, token_out_denom: alloyed_denom.clone(), @@ -1382,7 +1345,7 @@ fn test_register_limiter_after_having_liquidity() { cp.swap_exact_amount_in( MsgSwapExactAmountIn { sender: t.accounts["provider"].address(), - token_in: Some(Coin::new(1, AXL_USDC).into()), + token_in: Some(coin(1, AXL_USDC).into()), routes: vec![SwapAmountInRoute { pool_id: t.contract.pool_id, token_out_denom: alloyed_denom.clone(), @@ -1397,7 +1360,7 @@ fn test_register_limiter_after_having_liquidity() { cp.swap_exact_amount_in( MsgSwapExactAmountIn { sender: t.accounts["provider"].address(), - token_in: Some(Coin::new(1, &alloyed_denom).into()), + token_in: Some(coin(1, &alloyed_denom).into()), routes: vec![SwapAmountInRoute { pool_id: t.contract.pool_id, token_out_denom: COSMOS_USDC.to_string(), @@ -1412,7 +1375,7 @@ fn test_register_limiter_after_having_liquidity() { cp.swap_exact_amount_in( MsgSwapExactAmountIn { sender: t.accounts["provider"].address(), - token_in: Some(Coin::new(1, AXL_USDC).into()), + token_in: Some(coin(1, AXL_USDC).into()), routes: vec![SwapAmountInRoute { pool_id: t.contract.pool_id, token_out_denom: COSMOS_USDC.to_string(), @@ -1428,7 +1391,7 @@ fn test_register_limiter_after_having_liquidity() { .swap_exact_amount_in( MsgSwapExactAmountIn { sender: t.accounts["provider"].address(), - token_in: Some(Coin::new(1, COSMOS_USDC).into()), + token_in: Some(coin(1, COSMOS_USDC).into()), routes: vec![SwapAmountInRoute { pool_id: t.contract.pool_id, token_out_denom: AXL_USDC.to_string(), @@ -1452,7 +1415,7 @@ fn test_register_limiter_after_having_liquidity() { cp.swap_exact_amount_in( MsgSwapExactAmountIn { sender: t.accounts["provider"].address(), - token_in: Some(Coin::new(1, alloyed_denom).into()), + token_in: Some(coin(1, alloyed_denom).into()), routes: vec![SwapAmountInRoute { pool_id: t.contract.pool_id, token_out_denom: COSMOS_USDC.to_string(), diff --git a/contracts/transmuter/src/test/cases/units/add_new_assets.rs b/contracts/transmuter/src/test/cases/units/add_new_assets.rs index 4d46f4e..53bef42 100644 --- a/contracts/transmuter/src/test/cases/units/add_new_assets.rs +++ b/contracts/transmuter/src/test/cases/units/add_new_assets.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Coin, Uint128}; +use cosmwasm_std::{coin, Uint128}; use osmosis_test_tube::OsmosisTestApp; use crate::{ @@ -15,10 +15,10 @@ fn test_add_new_assets() { // create denom app.init_account(&[ - Coin::new(1, "denom1"), - Coin::new(1, "denom2"), - Coin::new(1, "denom3"), - Coin::new(1, "denom4"), + coin(1, "denom1"), + coin(1, "denom2"), + coin(1, "denom3"), + coin(1, "denom4"), ]) .unwrap(); @@ -81,10 +81,10 @@ fn test_add_new_assets() { assert_eq!( total_pool_liquidity, vec![ - Coin::new(0, "denom1"), - Coin::new(0, "denom2"), - Coin::new(0, "denom3"), - Coin::new(0, "denom4"), + coin(0, "denom1"), + coin(0, "denom2"), + coin(0, "denom3"), + coin(0, "denom4"), ] ); diff --git a/contracts/transmuter/src/test/cases/units/admin.rs b/contracts/transmuter/src/test/cases/units/admin.rs index 0cf66c4..26bd20c 100644 --- a/contracts/transmuter/src/test/cases/units/admin.rs +++ b/contracts/transmuter/src/test/cases/units/admin.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Coin, Uint128}; +use cosmwasm_std::{coin, Uint128}; use osmosis_std::types::cosmos::bank::v1beta1::{ DenomUnit, Metadata, QueryDenomMetadataRequest, QueryDenomMetadataResponse, @@ -21,12 +21,9 @@ fn test_admin_set_denom_metadata() { let alloyed_asset_subdenom = "eth"; let t = TestEnvBuilder::new() - .with_account("alice", vec![Coin::new(1_500, AXL_ETH)]) - .with_account("bob", vec![Coin::new(1_500, WH_ETH)]) - .with_account( - "admin", - vec![Coin::new(100_000, AXL_ETH), Coin::new(100_000, WH_ETH)], - ) + .with_account("alice", vec![coin(1_500, AXL_ETH)]) + .with_account("bob", vec![coin(1_500, WH_ETH)]) + .with_account("admin", vec![coin(100_000, AXL_ETH), coin(100_000, WH_ETH)]) .with_instantiate_msg(InstantiateMsg { pool_asset_configs: vec![ AssetConfig::from_denom_str(AXL_ETH), diff --git a/contracts/transmuter/src/test/cases/units/create_pool.rs b/contracts/transmuter/src/test/cases/units/create_pool.rs index 49786a8..3dfe82e 100644 --- a/contracts/transmuter/src/test/cases/units/create_pool.rs +++ b/contracts/transmuter/src/test/cases/units/create_pool.rs @@ -7,7 +7,7 @@ use crate::{ IsActiveResponse, }, }; -use cosmwasm_std::{Coin, Uint128}; +use cosmwasm_std::{coin, Uint128}; use osmosis_test_tube::OsmosisTestApp; use crate::test::test_env::TestEnvBuilder; @@ -17,7 +17,7 @@ fn test_create_pool() { let app = OsmosisTestApp::new(); // create denom - app.init_account(&[Coin::new(1, "denom1"), Coin::new(1, "denom2")]) + app.init_account(&[coin(1, "denom1"), coin(1, "denom2")]) .unwrap(); let t = TestEnvBuilder::new() @@ -52,10 +52,7 @@ fn test_create_pool() { assert_eq!( total_pool_liquidity, - vec![ - Coin::new(0, "denom1".to_string()), - Coin::new(0, "denom2".to_string()) - ] + vec![coin(0, "denom1".to_string()), coin(0, "denom2".to_string())] ); // get total shares diff --git a/contracts/transmuter/src/test/cases/units/join_and_exit.rs b/contracts/transmuter/src/test/cases/units/join_and_exit.rs index 214163a..ba5224a 100644 --- a/contracts/transmuter/src/test/cases/units/join_and_exit.rs +++ b/contracts/transmuter/src/test/cases/units/join_and_exit.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use cosmwasm_std::{attr, Coin, Uint128}; +use cosmwasm_std::{attr, coin, Coin, Uint128}; use itertools::Itertools; use osmosis_test_tube::{Account, OsmosisTestApp}; @@ -24,40 +24,34 @@ fn test_join_pool_with_single_lp_should_update_shares_and_liquidity_properly() { let cases = vec![ Case { - funds: vec![Coin::new(1, "denoma")], + funds: vec![coin(1, "denoma")], }, Case { - funds: vec![Coin::new(1, "denomb")], + funds: vec![coin(1, "denomb")], }, Case { - funds: vec![Coin::new(100, "denoma")], + funds: vec![coin(100, "denoma")], }, Case { - funds: vec![Coin::new(100, "denomb")], + funds: vec![coin(100, "denomb")], }, Case { - funds: vec![Coin::new(100_000_000_000_000, "denoma")], + funds: vec![coin(100_000_000_000_000, "denoma")], }, Case { - funds: vec![Coin::new(100_000_000_000_000, "denomb")], + funds: vec![coin(100_000_000_000_000, "denomb")], }, Case { - funds: vec![Coin::new(u128::MAX, "denoma")], + funds: vec![coin(u128::MAX, "denoma")], }, Case { - funds: vec![Coin::new(u128::MAX, "denomb")], + funds: vec![coin(u128::MAX, "denomb")], }, Case { - funds: vec![ - Coin::new(999_999_999, "denoma"), - Coin::new(999_999_999, "denomb"), - ], + funds: vec![coin(999_999_999, "denoma"), coin(999_999_999, "denomb")], }, Case { - funds: vec![ - Coin::new(12_000_000_000, "denoma"), - Coin::new(999_999_999, "denomb"), - ], + funds: vec![coin(12_000_000_000, "denoma"), coin(999_999_999, "denomb")], }, ]; @@ -84,7 +78,7 @@ fn test_join_pool_with_single_lp_should_update_shares_and_liquidity_properly() { // make supply non-zero for denom in missing_denoms { - app.init_account(&[Coin::new(1, denom)]).unwrap(); + app.init_account(&[coin(1, denom)]).unwrap(); } let t = TestEnvBuilder::new() @@ -161,41 +155,32 @@ fn test_join_pool_should_update_shares_and_liquidity_properly() { let cases = vec![ Case { joins: vec![ - ("addr1", vec![Coin::new(1, "denoma")]), - ("addr2", vec![Coin::new(1, "denomb")]), + ("addr1", vec![coin(1, "denoma")]), + ("addr2", vec![coin(1, "denomb")]), ], }, Case { - joins: vec![("addr1", vec![Coin::new(u128::MAX, "denoma")])], + joins: vec![("addr1", vec![coin(u128::MAX, "denoma")])], }, Case { - joins: vec![("addr1", vec![Coin::new(u128::MAX, "denomb")])], + joins: vec![("addr1", vec![coin(u128::MAX, "denomb")])], }, Case { joins: vec![ - ("addr1", vec![Coin::new(100_000, "denoma")]), - ("addr2", vec![Coin::new(999_999_999, "denomb")]), - ("addr3", vec![Coin::new(1, "denoma")]), - ("addr4", vec![Coin::new(2, "denomb")]), + ("addr1", vec![coin(100_000, "denoma")]), + ("addr2", vec![coin(999_999_999, "denomb")]), + ("addr3", vec![coin(1, "denoma")]), + ("addr4", vec![coin(2, "denomb")]), ], }, Case { joins: vec![ - ( - "addr1", - vec![Coin::new(100_000, "denoma"), Coin::new(999, "denomb")], - ), + ("addr1", vec![coin(100_000, "denoma"), coin(999, "denomb")]), ( "addr2", - vec![ - Coin::new(999_999_999, "denoma"), - Coin::new(999_999_999, "denomb"), - ], - ), - ( - "addr3", - vec![Coin::new(1, "denoma"), Coin::new(1, "denomb")], + vec![coin(999_999_999, "denoma"), coin(999_999_999, "denomb")], ), + ("addr3", vec![coin(1, "denoma"), coin(1, "denomb")]), ], }, ]; @@ -224,7 +209,7 @@ fn test_join_pool_should_update_shares_and_liquidity_properly() { // make supply non-zero for denom in missing_denoms { - app.init_account(&[Coin::new(1, denom)]).unwrap(); + app.init_account(&[coin(1, denom)]).unwrap(); } for (acc, funds) in case.joins.clone() { @@ -318,28 +303,28 @@ fn test_exit_pool_less_than_their_shares_should_update_shares_and_liquidity_prop let cases = vec![ Case { - join: vec![Coin::new(1, "denoma")], - exit: vec![Coin::new(1, "denoma")], + join: vec![coin(1, "denoma")], + exit: vec![coin(1, "denoma")], }, Case { - join: vec![Coin::new(100_000, "denoma"), Coin::new(1, "denomb")], - exit: vec![Coin::new(100_000, "denoma")], + join: vec![coin(100_000, "denoma"), coin(1, "denomb")], + exit: vec![coin(100_000, "denoma")], }, Case { - join: vec![Coin::new(1, "denoma"), Coin::new(100_000, "denomb")], - exit: vec![Coin::new(100_000, "denomb")], + join: vec![coin(1, "denoma"), coin(100_000, "denomb")], + exit: vec![coin(100_000, "denomb")], }, Case { - join: vec![Coin::new(u128::MAX, "denoma")], - exit: vec![Coin::new(u128::MAX, "denoma")], + join: vec![coin(u128::MAX, "denoma")], + exit: vec![coin(u128::MAX, "denoma")], }, Case { - join: vec![Coin::new(u128::MAX, "denoma")], - exit: vec![Coin::new(u128::MAX - 1, "denoma")], + join: vec![coin(u128::MAX, "denoma")], + exit: vec![coin(u128::MAX - 1, "denoma")], }, Case { - join: vec![Coin::new(u128::MAX, "denoma")], - exit: vec![Coin::new(u128::MAX - 100_000_000, "denoma")], + join: vec![coin(u128::MAX, "denoma")], + exit: vec![coin(u128::MAX - 100_000_000, "denoma")], }, ]; @@ -350,10 +335,7 @@ fn test_exit_pool_less_than_their_shares_should_update_shares_and_liquidity_prop .with_account("instantiator", vec![]) .with_account( "addr1", - vec![ - Coin::new(u128::MAX, "denoma"), - Coin::new(u128::MAX, "denomb"), - ], + vec![coin(u128::MAX, "denoma"), coin(u128::MAX, "denomb")], ) .with_instantiate_msg(InstantiateMsg { pool_asset_configs: vec![ @@ -455,6 +437,7 @@ fn test_exit_pool_less_than_their_shares_should_update_shares_and_liquidity_prop vec![ attr("burn_from_address", t.accounts["addr1"].address()), attr("amount", format!("{}{}", exit_amount, share_denom)), + attr("msg_index", "0"), ] ); @@ -509,7 +492,7 @@ fn test_exit_pool_less_than_their_shares_should_update_shares_and_liquidity_prop .exit .iter() .find(|coin| coin.denom == curr.denom) - .unwrap_or(&Coin::new(0, curr.denom)) + .unwrap_or(&coin(0, curr.denom)) .amount; assert_eq!(curr.amount, prev.amount - exit_amount); }); @@ -526,23 +509,23 @@ fn test_exit_pool_greater_than_their_shares_should_fail() { let cases = vec![ Case { - join: vec![Coin::new(1, "denoma")], - exit: vec![Coin::new(2, "denoma")], + join: vec![coin(1, "denoma")], + exit: vec![coin(2, "denoma")], }, Case { - join: vec![Coin::new(100_000_000, "denoma")], - exit: vec![Coin::new(100_000_001, "denoma")], + join: vec![coin(100_000_000, "denoma")], + exit: vec![coin(100_000_001, "denoma")], }, Case { - join: vec![Coin::new(u128::MAX - 1, "denoma")], - exit: vec![Coin::new(u128::MAX, "denoma")], + join: vec![coin(u128::MAX - 1, "denoma")], + exit: vec![coin(u128::MAX, "denoma")], }, Case { join: vec![ - Coin::new(u128::MAX - 100_000_000, "denoma"), - Coin::new(99_999_999, "denomb"), + coin(u128::MAX - 100_000_000, "denoma"), + coin(99_999_999, "denomb"), ], - exit: vec![Coin::new(u128::MAX, "denoma")], + exit: vec![coin(u128::MAX, "denoma")], }, ]; @@ -550,7 +533,7 @@ fn test_exit_pool_greater_than_their_shares_should_fail() { let app = OsmosisTestApp::new(); // create denom - app.init_account(&[Coin::new(1, "denoma"), Coin::new(1, "denomb")]) + app.init_account(&[coin(1, "denoma"), coin(1, "denomb")]) .unwrap(); let t = TestEnvBuilder::new() @@ -605,8 +588,8 @@ fn test_exit_pool_greater_than_their_shares_should_fail() { fn test_exit_pool_within_shares_but_over_joined_denom_amount() { let app = OsmosisTestApp::new(); let t = TestEnvBuilder::new() - .with_account("instantiator", vec![Coin::new(100_000_000, "denoma")]) - .with_account("addr1", vec![Coin::new(200_000_000, "denomb")]) + .with_account("instantiator", vec![coin(100_000_000, "denoma")]) + .with_account("addr1", vec![coin(200_000_000, "denomb")]) .with_instantiate_msg(InstantiateMsg { pool_asset_configs: vec![ AssetConfig::from_denom_str("denoma"), @@ -622,7 +605,7 @@ fn test_exit_pool_within_shares_but_over_joined_denom_amount() { t.contract .execute( &ExecMsg::JoinPool {}, - &[Coin::new(100_000_000, "denoma")], + &[coin(100_000_000, "denoma")], &t.accounts["instantiator"], ) .unwrap(); @@ -630,7 +613,7 @@ fn test_exit_pool_within_shares_but_over_joined_denom_amount() { t.contract .execute( &ExecMsg::JoinPool {}, - &[Coin::new(200_000_000, "denomb")], + &[coin(200_000_000, "denomb")], &t.accounts["addr1"], ) .unwrap(); @@ -638,10 +621,7 @@ fn test_exit_pool_within_shares_but_over_joined_denom_amount() { t.contract .execute( &ExecMsg::ExitPool { - tokens_out: vec![ - Coin::new(100_000_000, "denoma"), - Coin::new(100_000_000, "denomb"), - ], + tokens_out: vec![coin(100_000_000, "denoma"), coin(100_000_000, "denomb")], }, &[], &t.accounts["addr1"], diff --git a/contracts/transmuter/src/test/cases/units/migrate.rs b/contracts/transmuter/src/test/cases/units/migrate.rs index 1b47a5d..0c06d9d 100644 --- a/contracts/transmuter/src/test/cases/units/migrate.rs +++ b/contracts/transmuter/src/test/cases/units/migrate.rs @@ -10,7 +10,7 @@ use crate::{ test::{modules::cosmwasm_pool::CosmwasmPool, test_env::TransmuterContract}, }; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{from_json, to_json_binary, Coin, Uint128}; +use cosmwasm_std::{coin, from_json, to_json_binary, Uint128}; use osmosis_std::types::{ cosmwasm::wasm::v1::{QueryRawContractStateRequest, QueryRawContractStateResponse}, osmosis::cosmwasmpool::v1beta1::{ @@ -42,9 +42,9 @@ fn test_migrate_v2_to_v3() { let app = OsmosisTestApp::new(); let signer = app .init_account(&[ - Coin::new(100000, "denom1"), - Coin::new(100000, "denom2"), - Coin::new(10000000000000, "uosmo"), + coin(100000, "denom1"), + coin(100000, "denom2"), + coin(10000000000000, "uosmo"), ]) .unwrap(); @@ -143,7 +143,8 @@ fn test_migrate_v2_to_v3() { assert_eq!(asset_configs, expected_asset_configs); let GetModeratorResponse { moderator } = t.query(&QueryMsg::GetModerator {}).unwrap(); - assert_eq!(moderator, migrate_msg.moderator.unwrap()); + + assert_eq!(moderator.into_string(), migrate_msg.moderator.unwrap()); } #[cw_serde] @@ -157,9 +158,9 @@ fn test_migrate_v3_2(#[case] from_version: &str) { let app = OsmosisTestApp::new(); let signer = app .init_account(&[ - Coin::new(100000, "denom1"), - Coin::new(100000, "denom2"), - Coin::new(10000000000000, "uosmo"), + coin(100000, "denom1"), + coin(100000, "denom2"), + coin(10000000000000, "uosmo"), ]) .unwrap(); @@ -283,9 +284,9 @@ fn test_migrate_v4_0_0() { let app = OsmosisTestApp::new(); let signer = app .init_account(&[ - Coin::new(100000, "denom1"), - Coin::new(100000, "denom2"), - Coin::new(10000000000000, "uosmo"), + coin(100000, "denom1"), + coin(100000, "denom2"), + coin(10000000000000, "uosmo"), ]) .unwrap(); diff --git a/contracts/transmuter/src/test/cases/units/spot_price.rs b/contracts/transmuter/src/test/cases/units/spot_price.rs index 1f2e60d..3bb9934 100644 --- a/contracts/transmuter/src/test/cases/units/spot_price.rs +++ b/contracts/transmuter/src/test/cases/units/spot_price.rs @@ -1,5 +1,6 @@ use cosmwasm_std::{ - testing::{mock_dependencies, mock_env, mock_info}, + coin, + testing::{message_info, mock_dependencies, mock_env, mock_info}, Coin, Decimal, Uint128, }; use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx}; @@ -8,32 +9,31 @@ use crate::{asset::AssetConfig, contract::Transmuter, ContractError}; #[test] fn test_spot_price_on_balanced_liquidity_must_be_one() { - test_spot_price(&[Coin::new(100_000, "denom0"), Coin::new(100_000, "denom1")]) + test_spot_price(&[coin(100_000, "denom0"), coin(100_000, "denom1")]) } #[test] fn test_spot_price_on_unbalanced_liquidity_must_be_one() { - test_spot_price(&[ - Coin::new(999_999_999, "denom0"), - Coin::new(100_000, "denom1"), - ]) + test_spot_price(&[coin(999_999_999, "denom0"), coin(100_000, "denom1")]) } fn test_spot_price(liquidity: &[Coin]) { - let transmuter = Transmuter::default(); + let transmuter = Transmuter::new(); let mut deps = mock_dependencies(); + deps.api = deps.api.with_prefix("osmo"); // make denom has non-zero total supply - deps.querier.update_balance( - "someone", - vec![Coin::new(1, "denom0"), Coin::new(1, "denom1")], - ); + deps.querier + .bank + .update_balance("someone", vec![coin(1, "denom0"), coin(1, "denom1")]); + + let creator = deps.api.addr_make("creator"); transmuter .instantiate( InstantiateCtx { deps: deps.as_mut(), env: mock_env(), - info: mock_info("creator", &[]), + info: message_info(&creator, &[]), }, vec![ AssetConfig::from_denom_str("denom0"), diff --git a/contracts/transmuter/src/test/cases/units/swap/client_error.rs b/contracts/transmuter/src/test/cases/units/swap/client_error.rs index 29d4c17..7f5169a 100644 --- a/contracts/transmuter/src/test/cases/units/swap/client_error.rs +++ b/contracts/transmuter/src/test/cases/units/swap/client_error.rs @@ -1,3 +1,5 @@ +use cosmwasm_std::coin; + use super::*; const REMAINING_DENOM0: u128 = 1_000_000_000_000; @@ -7,8 +9,8 @@ fn pool_with_assets(app: &'_ OsmosisTestApp) -> TestEnv<'_> { pool_with_single_lp( app, vec![ - Coin::new(REMAINING_DENOM0, "denom0"), - Coin::new(REMAINING_DENOM1, "denom1"), + coin(REMAINING_DENOM0, "denom0"), + coin(REMAINING_DENOM1, "denom1"), ], vec![], ) @@ -18,7 +20,7 @@ test_swap! { swap_exact_amount_in_with_token_out_less_than_token_out_min_amount_should_fail [expect error] { setup = pool_with_assets, msg = SwapMsg::SwapExactAmountIn { - token_in: Coin::new(1_000_000, "denom0"), + token_in: coin(1_000_000, "denom0"), token_out_denom: "denom1".to_string(), token_out_min_amount: 1_000_001u128.into(), }, @@ -35,7 +37,7 @@ test_swap! { msg = SwapMsg::SwapExactAmountOut { token_in_denom: "denom0".to_string(), token_in_max_amount: 999_999u128.into(), - token_out: Coin::new(1_000_000, "denom1"), + token_out: coin(1_000_000, "denom1"), }, err = ContractError::ExcessiveRequiredTokenIn { limit: 999_999u128.into(), @@ -49,14 +51,14 @@ test_swap! { setup = pool_with_assets, msgs = [ SwapMsg::SwapExactAmountIn { - token_in: Coin::new(1_000_000, "denom0"), + token_in: coin(1_000_000, "denom0"), token_out_denom: "uosmo".to_string(), token_out_min_amount: Uint128::one(), }, SwapMsg::SwapExactAmountOut { token_in_denom: "denom0".to_string(), token_in_max_amount: 1_000_000u128.into(), - token_out: Coin::new(1_000_000, "uosmo"), + token_out: coin(1_000_000, "uosmo"), }, ], err = ContractError::InvalidTransmuteDenom { @@ -71,14 +73,14 @@ test_swap! { setup = pool_with_assets, msgs = [ SwapMsg::SwapExactAmountIn { - token_in: Coin::new(1_000_000, "uosmo"), + token_in: coin(1_000_000, "uosmo"), token_out_denom: "denom1".to_string(), token_out_min_amount: Uint128::one(), }, SwapMsg::SwapExactAmountOut { token_in_denom: "uosmo".to_string(), token_in_max_amount: 1_000_000u128.into(), - token_out: Coin::new(1_000_000, "denom1"), + token_out: coin(1_000_000, "denom1"), }, ], err = ContractError::InvalidTransmuteDenom { diff --git a/contracts/transmuter/src/test/cases/units/swap/mod.rs b/contracts/transmuter/src/test/cases/units/swap/mod.rs index bd954e7..e93a206 100644 --- a/contracts/transmuter/src/test/cases/units/swap/mod.rs +++ b/contracts/transmuter/src/test/cases/units/swap/mod.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Coin, Uint128}; +use cosmwasm_std::{coin, Coin, Uint128}; use osmosis_std::types::cosmos::bank::v1beta1::QueryBalanceRequest; use osmosis_std::types::osmosis::poolmanager::v1beta1::{ @@ -495,7 +495,7 @@ fn pool_with_single_lp( non_zero_pool_assets .iter() .filter(|coin| !coin.amount.is_zero()) - .map(|coin| Coin::new(10000000000000000000000000, coin.denom.clone())) + .map(|c| coin(10000000000000000000000000, c.denom.clone())) .collect(), ) .with_instantiate_msg(crate::contract::sv::InstantiateMsg { diff --git a/contracts/transmuter/src/test/cases/units/swap/non_empty_pool.rs b/contracts/transmuter/src/test/cases/units/swap/non_empty_pool.rs index 2b2ea3a..3ba4c24 100644 --- a/contracts/transmuter/src/test/cases/units/swap/non_empty_pool.rs +++ b/contracts/transmuter/src/test/cases/units/swap/non_empty_pool.rs @@ -1,3 +1,5 @@ +use cosmwasm_std::coin; + use super::*; const REMAINING_DENOM0: u128 = 1_000_000_000_000_000_000; @@ -7,8 +9,8 @@ fn non_empty_pool(app: &'_ OsmosisTestApp) -> TestEnv<'_> { pool_with_single_lp( app, vec![ - Coin::new(REMAINING_DENOM0, "denom0"), - Coin::new(REMAINING_DENOM1, "denom1"), + coin(REMAINING_DENOM0, "denom0"), + coin(REMAINING_DENOM1, "denom1"), ], vec![], ) @@ -19,17 +21,17 @@ test_swap! { setup = non_empty_pool, msgs = [ SwapMsg::SwapExactAmountIn { - token_in: Coin::new(1, "denom0"), + token_in: coin(1, "denom0"), token_out_denom: "denom1".to_string(), token_out_min_amount: Uint128::one(), }, SwapMsg::SwapExactAmountOut { token_in_denom: "denom0".to_string(), token_in_max_amount: Uint128::one(), - token_out: Coin::new(1, "denom1"), + token_out: coin(1, "denom1"), }, ], - received = Coin::new(1, "denom1") + received = coin(1, "denom1") } } @@ -38,17 +40,17 @@ test_swap! { setup = non_empty_pool, msgs = [ SwapMsg::SwapExactAmountIn { - token_in: Coin::new(1, "denom1"), + token_in: coin(1, "denom1"), token_out_denom: "denom0".to_string(), token_out_min_amount: Uint128::one(), }, SwapMsg::SwapExactAmountOut { token_in_denom: "denom1".to_string(), token_in_max_amount: Uint128::one(), - token_out: Coin::new(1, "denom0"), + token_out: coin(1, "denom0"), }, ], - received = Coin::new(1, "denom0") + received = coin(1, "denom0") } } @@ -57,17 +59,17 @@ test_swap! { setup = non_empty_pool, msgs = [ SwapMsg::SwapExactAmountIn { - token_in: Coin::new(REMAINING_DENOM0, "denom0"), + token_in: coin(REMAINING_DENOM0, "denom0"), token_out_denom: "denom1".to_string(), token_out_min_amount: REMAINING_DENOM0.into(), }, SwapMsg::SwapExactAmountOut { token_in_denom: "denom0".to_string(), token_in_max_amount: REMAINING_DENOM0.into(), - token_out: Coin::new(REMAINING_DENOM0, "denom1"), + token_out: coin(REMAINING_DENOM0, "denom1"), }, ], - received = Coin::new(REMAINING_DENOM0, "denom1") + received = coin(REMAINING_DENOM0, "denom1") } } @@ -76,17 +78,17 @@ test_swap! { setup = non_empty_pool, msgs = [ SwapMsg::SwapExactAmountIn { - token_in: Coin::new(REMAINING_DENOM1, "denom1"), + token_in: coin(REMAINING_DENOM1, "denom1"), token_out_denom: "denom0".to_string(), token_out_min_amount: REMAINING_DENOM1.into(), }, SwapMsg::SwapExactAmountOut { token_in_denom: "denom1".to_string(), token_in_max_amount: REMAINING_DENOM1.into(), - token_out: Coin::new(REMAINING_DENOM1, "denom0"), + token_out: coin(REMAINING_DENOM1, "denom0"), }, ], - received = Coin::new(REMAINING_DENOM1, "denom0") + received = coin(REMAINING_DENOM1, "denom0") } } @@ -95,17 +97,17 @@ test_swap! { setup = non_empty_pool, msgs = [ SwapMsg::SwapExactAmountIn { - token_in: Coin::new(999_999, "denom1"), + token_in: coin(999_999, "denom1"), token_out_denom: "denom0".to_string(), token_out_min_amount: 999_999u128.into(), }, SwapMsg::SwapExactAmountOut { token_in_denom: "denom1".to_string(), token_in_max_amount: 999_999u128.into(), - token_out: Coin::new(999_999, "denom0"), + token_out: coin(999_999, "denom0"), }, ], - received = Coin::new(999_999, "denom0") + received = coin(999_999, "denom0") } } @@ -114,17 +116,17 @@ test_swap! { setup = non_empty_pool, msgs = [ SwapMsg::SwapExactAmountIn { - token_in: Coin::new(999_999, "denom0"), + token_in: coin(999_999, "denom0"), token_out_denom: "denom1".to_string(), token_out_min_amount: 999_999u128.into(), }, SwapMsg::SwapExactAmountOut { token_in_denom: "denom0".to_string(), token_in_max_amount: 999_999u128.into(), - token_out: Coin::new(999_999, "denom1"), + token_out: coin(999_999, "denom1"), }, ], - received = Coin::new(999_999, "denom1") + received = coin(999_999, "denom1") } } @@ -132,8 +134,8 @@ fn non_empty_pool_with_normalization_factor(app: &'_ OsmosisTestApp) -> TestEnv< pool_with_single_lp( app, vec![ - Coin::new(REMAINING_DENOM0, "denom0"), - Coin::new(REMAINING_DENOM1, "denom1"), + coin(REMAINING_DENOM0, "denom0"), + coin(REMAINING_DENOM1, "denom1"), ], vec![ AssetConfig { @@ -153,17 +155,17 @@ test_swap! { setup = non_empty_pool_with_normalization_factor, msgs = [ SwapMsg::SwapExactAmountIn { - token_in: Coin::new(3u128 * 10u128.pow(2), "denom0"), + token_in: coin(3u128 * 10u128.pow(2), "denom0"), token_out_denom: "denom1".to_string(), token_out_min_amount: Uint128::one(), }, SwapMsg::SwapExactAmountOut { token_in_denom: "denom0".to_string(), token_in_max_amount: (300u128).into(), - token_out: Coin::new(1, "denom1"), + token_out: coin(1, "denom1"), }, ], - received = Coin::new(1, "denom1") + received = coin(1, "denom1") } } @@ -172,13 +174,13 @@ test_swap! { setup = non_empty_pool_with_normalization_factor, msgs = [ SwapMsg::SwapExactAmountIn { - token_in: Coin::new(1000, "denom0"), + token_in: coin(1000, "denom0"), token_out_denom: "denom1".to_string(), token_out_min_amount: Uint128::from(3u128), }, ], - received = Coin::new(3, "denom1") + received = coin(3, "denom1") } } @@ -189,9 +191,9 @@ test_swap! { SwapMsg::SwapExactAmountOut { token_in_denom: "denom0".to_string(), token_in_max_amount: (900u128).into(), - token_out: Coin::new(3, "denom1"), + token_out: coin(3, "denom1"), }, ], - received = Coin::new(3, "denom1") + received = coin(3, "denom1") } } diff --git a/contracts/transmuter/src/test/cases/units/swap/swap_share_denom.rs b/contracts/transmuter/src/test/cases/units/swap/swap_share_denom.rs index 6304d48..ec79e0d 100644 --- a/contracts/transmuter/src/test/cases/units/swap/swap_share_denom.rs +++ b/contracts/transmuter/src/test/cases/units/swap/swap_share_denom.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Coin, Uint128}; +use cosmwasm_std::{coin, Uint128}; use crate::{asset::AssetConfig, contract::sv::QueryMsg, contract::GetShareDenomResponse}; @@ -12,8 +12,8 @@ fn test_swap_exact_amount_in_with_share_denom() { let t = pool_with_single_lp( &app, vec![ - Coin::new(REMAINING_DENOM0, "denom0"), - Coin::new(REMAINING_DENOM1, "denom1"), + coin(REMAINING_DENOM0, "denom0"), + coin(REMAINING_DENOM1, "denom1"), ], vec![], ); @@ -28,23 +28,23 @@ fn test_swap_exact_amount_in_with_share_denom() { test_swap_share_denom_success_case( &t, SwapMsg::SwapExactAmountIn { - token_in: Coin::new(1_000, "denom0".to_string()), + token_in: coin(1_000, "denom0".to_string()), token_out_denom: share_denom.clone(), token_out_min_amount: Uint128::one(), }, - Coin::new(1_000, "denom0".to_string()), - Coin::new(1_000, share_denom.clone()), + coin(1_000, "denom0".to_string()), + coin(1_000, share_denom.clone()), ); test_swap_share_denom_success_case( &t, SwapMsg::SwapExactAmountIn { - token_in: Coin::new(1_000, share_denom.clone()), + token_in: coin(1_000, share_denom.clone()), token_out_denom: "denom1".to_string(), token_out_min_amount: Uint128::one(), }, - Coin::new(1_000, share_denom), - Coin::new(1_000, "denom1".to_string()), + coin(1_000, share_denom), + coin(1_000, "denom1".to_string()), ); } @@ -54,8 +54,8 @@ fn test_swap_exact_amount_in_with_share_denom_and_normalization_factor() { let t = pool_with_single_lp( &app, vec![ - Coin::new(REMAINING_DENOM0, "denom0"), - Coin::new(REMAINING_DENOM1 * 10u128.pow(8), "denom1"), + coin(REMAINING_DENOM0, "denom0"), + coin(REMAINING_DENOM1 * 10u128.pow(8), "denom1"), ], vec![AssetConfig { denom: "denom1".to_string(), @@ -73,23 +73,23 @@ fn test_swap_exact_amount_in_with_share_denom_and_normalization_factor() { test_swap_share_denom_success_case( &t, SwapMsg::SwapExactAmountIn { - token_in: Coin::new(1_000 * 10u128.pow(8), "denom1".to_string()), + token_in: coin(1_000 * 10u128.pow(8), "denom1".to_string()), token_out_denom: share_denom.clone(), token_out_min_amount: Uint128::one(), }, - Coin::new(1_000 * 10u128.pow(8), "denom1".to_string()), - Coin::new(1_000, share_denom.clone()), + coin(1_000 * 10u128.pow(8), "denom1".to_string()), + coin(1_000, share_denom.clone()), ); test_swap_share_denom_success_case( &t, SwapMsg::SwapExactAmountIn { - token_in: Coin::new(1000, share_denom.clone()), + token_in: coin(1000, share_denom.clone()), token_out_denom: "denom1".to_string(), token_out_min_amount: Uint128::one(), }, - Coin::new(1000, share_denom), - Coin::new(1000 * 10u128.pow(8), "denom1".to_string()), + coin(1000, share_denom), + coin(1000 * 10u128.pow(8), "denom1".to_string()), ); } @@ -99,8 +99,8 @@ fn test_swap_exact_amount_out_with_share_denom() { let t = pool_with_single_lp( &app, vec![ - Coin::new(REMAINING_DENOM0, "denom0"), - Coin::new(REMAINING_DENOM1, "denom1"), + coin(REMAINING_DENOM0, "denom0"), + coin(REMAINING_DENOM1, "denom1"), ], vec![], ); @@ -117,10 +117,10 @@ fn test_swap_exact_amount_out_with_share_denom() { SwapMsg::SwapExactAmountOut { token_in_denom: "denom0".to_string(), token_in_max_amount: Uint128::from(1_000u128), - token_out: Coin::new(1_000, share_denom.clone()), + token_out: coin(1_000, share_denom.clone()), }, - Coin::new(1_000, "denom0".to_string()), - Coin::new(1_000, share_denom.clone()), + coin(1_000, "denom0".to_string()), + coin(1_000, share_denom.clone()), ); test_swap_share_denom_success_case( @@ -128,17 +128,17 @@ fn test_swap_exact_amount_out_with_share_denom() { SwapMsg::SwapExactAmountOut { token_in_denom: share_denom.clone(), token_in_max_amount: Uint128::from(1_000u128), - token_out: Coin::new(1_000, "denom1".to_string()), + token_out: coin(1_000, "denom1".to_string()), }, - Coin::new(1_000, share_denom), - Coin::new(1_000, "denom1".to_string()), + coin(1_000, share_denom), + coin(1_000, "denom1".to_string()), ); } #[test] fn test_swap_exact_amount_out_with_share_denom_single_asset_pool() { let app = osmosis_test_tube::OsmosisTestApp::new(); - let t = pool_with_single_lp(&app, vec![Coin::new(REMAINING_DENOM0, "denom0")], vec![]); + let t = pool_with_single_lp(&app, vec![coin(REMAINING_DENOM0, "denom0")], vec![]); // get share denom let share_denom = t @@ -152,10 +152,10 @@ fn test_swap_exact_amount_out_with_share_denom_single_asset_pool() { SwapMsg::SwapExactAmountOut { token_in_denom: "denom0".to_string(), token_in_max_amount: Uint128::from(1_000u128), - token_out: Coin::new(1_000, share_denom.clone()), + token_out: coin(1_000, share_denom.clone()), }, - Coin::new(1_000, "denom0".to_string()), - Coin::new(1_000, share_denom.clone()), + coin(1_000, "denom0".to_string()), + coin(1_000, share_denom.clone()), ); test_swap_share_denom_success_case( @@ -163,10 +163,10 @@ fn test_swap_exact_amount_out_with_share_denom_single_asset_pool() { SwapMsg::SwapExactAmountOut { token_in_denom: share_denom.clone(), token_in_max_amount: Uint128::from(1_000u128), - token_out: Coin::new(1_000, "denom0".to_string()), + token_out: coin(1_000, "denom0".to_string()), }, - Coin::new(1_000, share_denom.clone()), - Coin::new(1_000, "denom0".to_string()), + coin(1_000, share_denom.clone()), + coin(1_000, "denom0".to_string()), ); } @@ -175,7 +175,7 @@ fn test_swap_exact_amount_in_with_share_denom_and_normalization_factor_single_as let app = osmosis_test_tube::OsmosisTestApp::new(); let t = pool_with_single_lp( &app, - vec![Coin::new(REMAINING_DENOM1 * 10u128.pow(8), "denom1")], + vec![coin(REMAINING_DENOM1 * 10u128.pow(8), "denom1")], vec![AssetConfig { denom: "denom1".to_string(), normalization_factor: 10u128.pow(8).into(), @@ -192,23 +192,23 @@ fn test_swap_exact_amount_in_with_share_denom_and_normalization_factor_single_as test_swap_share_denom_success_case( &t, SwapMsg::SwapExactAmountIn { - token_in: Coin::new(1_000 * 10u128.pow(8), "denom1".to_string()), + token_in: coin(1_000 * 10u128.pow(8), "denom1".to_string()), token_out_denom: share_denom.clone(), token_out_min_amount: Uint128::one(), }, - Coin::new(1_000 * 10u128.pow(8), "denom1".to_string()), - Coin::new(1_000, share_denom.clone()), + coin(1_000 * 10u128.pow(8), "denom1".to_string()), + coin(1_000, share_denom.clone()), ); test_swap_share_denom_success_case( &t, SwapMsg::SwapExactAmountIn { - token_in: Coin::new(1000, share_denom.clone()), + token_in: coin(1000, share_denom.clone()), token_out_denom: "denom1".to_string(), token_out_min_amount: Uint128::one(), }, - Coin::new(1000, share_denom), - Coin::new(1000 * 10u128.pow(8), "denom1".to_string()), + coin(1000, share_denom), + coin(1000 * 10u128.pow(8), "denom1".to_string()), ); } @@ -218,8 +218,8 @@ fn test_swap_exact_amount_out_with_share_denom_where_token_in_max_is_exceeding_e let t = pool_with_single_lp( &app, vec![ - Coin::new(REMAINING_DENOM0, "denom0"), - Coin::new(REMAINING_DENOM1, "denom1"), + coin(REMAINING_DENOM0, "denom0"), + coin(REMAINING_DENOM1, "denom1"), ], vec![], ); @@ -237,10 +237,10 @@ fn test_swap_exact_amount_out_with_share_denom_where_token_in_max_is_exceeding_e SwapMsg::SwapExactAmountOut { token_in_denom: "denom0".to_string(), token_in_max_amount: Uint128::from(1_001u128), - token_out: Coin::new(1_001, share_denom.clone()), + token_out: coin(1_001, share_denom.clone()), }, - Coin::new(1_001, "denom0".to_string()), - Coin::new(1_001, share_denom.clone()), + coin(1_001, "denom0".to_string()), + coin(1_001, share_denom.clone()), ); // swap 1000 share_denom for 1000 denom1 but set token_in_max_amount to 1001 (1 extra share_denom) @@ -249,9 +249,9 @@ fn test_swap_exact_amount_out_with_share_denom_where_token_in_max_is_exceeding_e SwapMsg::SwapExactAmountOut { token_in_denom: share_denom.clone(), token_in_max_amount: Uint128::from(1_001u128), - token_out: Coin::new(1_000, "denom1".to_string()), + token_out: coin(1_000, "denom1".to_string()), }, - Coin::new(1_000, share_denom), - Coin::new(1_000, "denom1".to_string()), + coin(1_000, share_denom), + coin(1_000, "denom1".to_string()), ); } diff --git a/contracts/transmuter/src/test/cases/units/swap/swap_with_asset_group_limiters.rs b/contracts/transmuter/src/test/cases/units/swap/swap_with_asset_group_limiters.rs index 1842b75..c8074fd 100644 --- a/contracts/transmuter/src/test/cases/units/swap/swap_with_asset_group_limiters.rs +++ b/contracts/transmuter/src/test/cases/units/swap/swap_with_asset_group_limiters.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Coin, Decimal, Uint128, Uint64}; +use cosmwasm_std::{coin, Decimal, Uint128, Uint64}; use crate::{ asset::AssetConfig, @@ -20,9 +20,9 @@ fn test_swap_with_asset_group_limiters() { let t = pool_with_single_lp( &app, vec![ - Coin::new(REMAINING_DENOM0, "denom0"), - Coin::new(REMAINING_DENOM1, "denom1"), - Coin::new(REMAINING_DENOM2, "denom2"), + coin(REMAINING_DENOM0, "denom0"), + coin(REMAINING_DENOM1, "denom1"), + coin(REMAINING_DENOM2, "denom2"), ], vec![ AssetConfig { @@ -76,9 +76,9 @@ fn test_swap_with_asset_group_limiters() { SwapMsg::SwapExactAmountOut { token_in_denom: "denom0".to_string(), token_in_max_amount: Uint128::from(REMAINING_DENOM1), - token_out: Coin::new(REMAINING_DENOM1, "denom1".to_string()), + token_out: coin(REMAINING_DENOM1, "denom1".to_string()), }, - Coin::new(REMAINING_DENOM1, "denom1".to_string()), + coin(REMAINING_DENOM1, "denom1".to_string()), ); app.increase_time(5); @@ -89,7 +89,7 @@ fn test_swap_with_asset_group_limiters() { SwapMsg::SwapExactAmountOut { token_in_denom: "denom0".to_string(), token_in_max_amount: Uint128::from(REMAINING_DENOM0), - token_out: Coin::new(REMAINING_DENOM0, "denom2".to_string()), + token_out: coin(REMAINING_DENOM0, "denom2".to_string()), }, ContractError::UpperLimitExceeded { scope: Scope::asset_group("group1"), diff --git a/contracts/transmuter/src/test/test_env.rs b/contracts/transmuter/src/test/test_env.rs index 9390cff..371f67b 100644 --- a/contracts/transmuter/src/test/test_env.rs +++ b/contracts/transmuter/src/test/test_env.rs @@ -5,7 +5,7 @@ use crate::{ ContractError, }; -use cosmwasm_std::{to_json_binary, Coin}; +use cosmwasm_std::{coin, to_json_binary, Coin}; use osmosis_std::types::{ cosmos::bank::v1beta1::QueryAllBalancesRequest, cosmwasm::wasm::v1::MsgExecuteContractResponse, @@ -45,12 +45,13 @@ impl<'a> TestEnv<'a> { .query_all_balances(&QueryAllBalancesRequest { address: self.accounts.get(account).unwrap().address(), pagination: None, + resolve_denom: false, }) .unwrap() .balances .into_iter() - .map(|coin| Coin::new(coin.amount.parse().unwrap(), coin.denom)) - .filter(|coin| !ignore_denoms.contains(&coin.denom.as_str())) + .map(|c| coin(c.amount.parse().unwrap(), c.denom)) + .filter(|c| !ignore_denoms.contains(&c.denom.as_str())) .collect(); assert_eq!(account_balances, expected_balances); @@ -61,11 +62,12 @@ impl<'a> TestEnv<'a> { .query_all_balances(&QueryAllBalancesRequest { address: self.contract.contract_addr.clone(), pagination: None, + resolve_denom: false, }) .unwrap() .balances .into_iter() - .map(|coin| Coin::new(coin.amount.parse().unwrap(), coin.denom)) + .map(|c| coin(c.amount.parse().unwrap(), c.denom)) .collect(); assert_eq!(contract_balances, expected_balances); @@ -108,7 +110,7 @@ impl TestEnvBuilder { .map(|(account, balance)| { let balance: Vec<_> = balance .into_iter() - .chain(vec![Coin::new(1000000000000, "uosmo")]) + .chain(vec![coin(1000000000000, "uosmo")]) .collect(); (account, app.init_account(&balance).unwrap()) @@ -116,7 +118,7 @@ impl TestEnvBuilder { .collect(); let creator = app - .init_account(&[Coin::new(1000000000000000u128, "uosmo")]) + .init_account(&[coin(1000000000000000u128, "uosmo")]) .unwrap(); let instantiate_msg = self.instantiate_msg.expect("instantiate msg not set"); diff --git a/contracts/transmuter/src/transmuter_pool/add_new_assets.rs b/contracts/transmuter/src/transmuter_pool/add_new_assets.rs index e3cf377..307bf03 100644 --- a/contracts/transmuter/src/transmuter_pool/add_new_assets.rs +++ b/contracts/transmuter/src/transmuter_pool/add_new_assets.rs @@ -15,7 +15,7 @@ impl TransmuterPool { mod tests { use std::collections::BTreeMap; - use cosmwasm_std::{Coin, Uint128, Uint64}; + use cosmwasm_std::{coin, Uint128, Uint64}; use crate::transmuter_pool::{MAX_POOL_ASSET_DENOMS, MIN_POOL_ASSET_DENOMS}; @@ -25,15 +25,13 @@ mod tests { fn test_add_new_assets() { let mut pool = TransmuterPool { pool_assets: Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(100000000, "asset1"), - Coin::new(99999999, "asset2"), + coin(100000000, "asset1"), + coin(99999999, "asset2"), ]), asset_groups: BTreeMap::new(), }; - let new_assets = Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(0, "asset3"), - Coin::new(0, "asset4"), - ]); + let new_assets = + Asset::unchecked_equal_assets_from_coins(&[coin(0, "asset3"), coin(0, "asset4")]); pool.add_new_assets(new_assets).unwrap(); assert_eq!( pool.pool_assets, @@ -50,15 +48,13 @@ mod tests { fn test_add_duplicated_assets() { let mut pool = TransmuterPool { pool_assets: Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(100000000, "asset1"), - Coin::new(99999999, "asset2"), + coin(100000000, "asset1"), + coin(99999999, "asset2"), ]), asset_groups: BTreeMap::new(), }; - let new_assets = Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(0, "asset3"), - Coin::new(0, "asset4"), - ]); + let new_assets = + Asset::unchecked_equal_assets_from_coins(&[coin(0, "asset3"), coin(0, "asset4")]); pool.add_new_assets(new_assets.clone()).unwrap(); let err = pool.add_new_assets(new_assets).unwrap_err(); assert_eq!( @@ -73,15 +69,15 @@ mod tests { fn test_add_same_new_assets() { let mut pool = TransmuterPool { pool_assets: Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(100000000, "asset1"), - Coin::new(99999999, "asset2"), + coin(100000000, "asset1"), + coin(99999999, "asset2"), ]), asset_groups: BTreeMap::new(), }; let err = pool .add_new_assets(Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(0, "asset5"), - Coin::new(0, "asset5"), + coin(0, "asset5"), + coin(0, "asset5"), ])) .unwrap_err(); assert_eq!( @@ -96,31 +92,31 @@ mod tests { fn test_pool_asset_out_of_range() { let mut pool = TransmuterPool { pool_assets: Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(100000000, "asset1"), - Coin::new(99999999, "asset2"), + coin(100000000, "asset1"), + coin(99999999, "asset2"), ]), asset_groups: BTreeMap::new(), }; let new_assets = Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(0, "asset3"), - Coin::new(0, "asset4"), - Coin::new(0, "asset5"), - Coin::new(0, "asset6"), - Coin::new(0, "asset7"), - Coin::new(0, "asset8"), - Coin::new(0, "asset9"), - Coin::new(0, "asset10"), - Coin::new(0, "asset11"), - Coin::new(0, "asset12"), - Coin::new(0, "asset13"), - Coin::new(0, "asset14"), - Coin::new(0, "asset15"), - Coin::new(0, "asset16"), - Coin::new(0, "asset17"), - Coin::new(0, "asset18"), - Coin::new(0, "asset19"), - Coin::new(0, "asset20"), - Coin::new(0, "asset21"), + coin(0, "asset3"), + coin(0, "asset4"), + coin(0, "asset5"), + coin(0, "asset6"), + coin(0, "asset7"), + coin(0, "asset8"), + coin(0, "asset9"), + coin(0, "asset10"), + coin(0, "asset11"), + coin(0, "asset12"), + coin(0, "asset13"), + coin(0, "asset14"), + coin(0, "asset15"), + coin(0, "asset16"), + coin(0, "asset17"), + coin(0, "asset18"), + coin(0, "asset19"), + coin(0, "asset20"), + coin(0, "asset21"), ]); let err = pool.add_new_assets(new_assets).unwrap_err(); assert_eq!( diff --git a/contracts/transmuter/src/transmuter_pool/asset_group.rs b/contracts/transmuter/src/transmuter_pool/asset_group.rs index a2959bb..921ab05 100644 --- a/contracts/transmuter/src/transmuter_pool/asset_group.rs +++ b/contracts/transmuter/src/transmuter_pool/asset_group.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashSet}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ensure, Decimal, Uint64}; @@ -99,7 +99,9 @@ impl TransmuterPool { } ); - // ensure that all denoms are valid pool assets + // ensure that all denoms are valid pool assets and has no duplicated denoms + // ensuring no duplicated denoms also ensures that it's within MAX_POOL_ASSET_DENOMS limit + let mut denoms_set = HashSet::new(); for denom in &denoms { ensure!( self.has_denom(denom), @@ -107,6 +109,12 @@ impl TransmuterPool { denom: denom.clone() } ); + ensure!( + denoms_set.insert(denom.clone()), + ContractError::DuplicatedPoolAssetDenom { + denom: denom.clone() + } + ); } self.asset_groups.insert(label, AssetGroup::new(denoms)); @@ -243,6 +251,29 @@ mod tests { ); } + #[test] + fn test_create_asset_group_with_duplicated_denom() { + let mut pool = TransmuterPool::new(vec![ + Asset::new(Uint128::new(100), "denom1", Uint128::new(1)).unwrap(), + Asset::new(Uint128::new(200), "denom2", Uint128::new(1)).unwrap(), + ]) + .unwrap(); + + let err = pool + .create_asset_group( + "group1".to_string(), + vec!["denom1".to_string(), "denom1".to_string()], + ) + .unwrap_err(); + + assert_eq!( + err, + ContractError::DuplicatedPoolAssetDenom { + denom: "denom1".to_string() + } + ); + } + #[test] fn test_create_asset_group_within_range() { let mut pool = TransmuterPool::new(vec![ diff --git a/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs b/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs index 20c4a18..382125f 100644 --- a/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs +++ b/contracts/transmuter/src/transmuter_pool/corrupted_assets.rs @@ -275,7 +275,7 @@ impl TransmuterPool { #[cfg(test)] mod tests { use crate::asset::Asset; - use cosmwasm_std::{Coin, Uint128}; + use cosmwasm_std::{coin, Uint128}; use super::*; @@ -283,10 +283,10 @@ mod tests { fn test_mark_corrupted_assets() { let mut pool = TransmuterPool { pool_assets: Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(100000000, "asset1"), - Coin::new(99999999, "asset2"), - Coin::new(1, "asset3"), - Coin::new(0, "asset4"), + coin(100000000, "asset1"), + coin(99999999, "asset2"), + coin(1, "asset3"), + coin(0, "asset4"), ]), asset_groups: BTreeMap::new(), }; @@ -312,10 +312,10 @@ mod tests { assert_eq!( pool.pool_assets, Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(100000000, "asset1"), - Coin::new(99999999, "asset2"), - Coin::new(1, "asset3"), - Coin::new(0, "asset4"), + coin(100000000, "asset1"), + coin(99999999, "asset2"), + coin(1, "asset3"), + coin(0, "asset4"), ]) .into_iter() .map(|asset| { @@ -341,10 +341,10 @@ mod tests { assert_eq!( pool.pool_assets, Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(100000000, "asset1"), - Coin::new(99999999, "asset2"), - Coin::new(1, "asset3"), - Coin::new(0, "asset4"), + coin(100000000, "asset1"), + coin(99999999, "asset2"), + coin(1, "asset3"), + coin(0, "asset4"), ]) .into_iter() .map(|asset| { @@ -372,10 +372,10 @@ mod tests { fn test_enforce_corrupted_asset_protocol() { let mut pool = TransmuterPool { pool_assets: Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(99999999, "asset1"), - Coin::new(100000000, "asset2"), - Coin::new(1, "asset3"), - Coin::new(0, "asset4"), + coin(99999999, "asset1"), + coin(100000000, "asset2"), + coin(1, "asset3"), + coin(0, "asset4"), ]), asset_groups: BTreeMap::new(), }; @@ -447,10 +447,10 @@ mod tests { // reset the pool because pure rust test will not reset state on error let mut pool = TransmuterPool { pool_assets: Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(99999999, "asset1"), - Coin::new(100000000, "asset2"), - Coin::new(1, "asset3"), - Coin::new(0, "asset4"), + coin(99999999, "asset1"), + coin(100000000, "asset2"), + coin(1, "asset3"), + coin(0, "asset4"), ]), asset_groups: BTreeMap::new(), }; @@ -476,10 +476,10 @@ mod tests { assert_eq!( pool.pool_assets, Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(99999997, "asset1"), - Coin::new(99999999, "asset2"), - Coin::new(1, "asset3"), - Coin::new(0, "asset4"), + coin(99999997, "asset1"), + coin(99999999, "asset2"), + coin(1, "asset3"), + coin(0, "asset4"), ]) .into_iter() .map(|asset| { @@ -613,9 +613,9 @@ mod tests { fn test_remove_corrupted_asset() { let mut pool = TransmuterPool { pool_assets: Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(100, "asset1"), - Coin::new(200, "asset2"), - Coin::new(300, "asset3"), + coin(100, "asset1"), + coin(200, "asset2"), + coin(300, "asset3"), ]), asset_groups: BTreeMap::from_iter(vec![( "group1".to_string(), diff --git a/contracts/transmuter/src/transmuter_pool/exit_pool.rs b/contracts/transmuter/src/transmuter_pool/exit_pool.rs index a0975ec..6273dac 100644 --- a/contracts/transmuter/src/transmuter_pool/exit_pool.rs +++ b/contracts/transmuter/src/transmuter_pool/exit_pool.rs @@ -46,6 +46,8 @@ impl TransmuterPool { mod tests { use std::collections::BTreeMap; + use cosmwasm_std::coin; + use crate::asset::Asset; use super::*; @@ -56,47 +58,47 @@ mod tests { fn test_exit_pool_succeed_when_has_enough_coins_in_pool() { let mut pool = TransmuterPool { pool_assets: Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(100_000, ETH_USDC), - Coin::new(100_000, COSMOS_USDC), + coin(100_000, ETH_USDC), + coin(100_000, COSMOS_USDC), ]), asset_groups: BTreeMap::new(), }; // exit pool with first token - pool.exit_pool(&[Coin::new(10_000, ETH_USDC)]).unwrap(); + pool.exit_pool(&[coin(10_000, ETH_USDC)]).unwrap(); assert_eq!( pool, TransmuterPool { pool_assets: Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(90_000, ETH_USDC), - Coin::new(100_000, COSMOS_USDC), + coin(90_000, ETH_USDC), + coin(100_000, COSMOS_USDC), ]), asset_groups: BTreeMap::new(), } ); // exit pool with second token - pool.exit_pool(&[Coin::new(10_000, COSMOS_USDC)]).unwrap(); + pool.exit_pool(&[coin(10_000, COSMOS_USDC)]).unwrap(); assert_eq!( pool, TransmuterPool { pool_assets: Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(90_000, ETH_USDC), - Coin::new(90_000, COSMOS_USDC), + coin(90_000, ETH_USDC), + coin(90_000, COSMOS_USDC), ]), asset_groups: BTreeMap::new(), } ); // exit pool with both tokens - pool.exit_pool(&[Coin::new(90_000, ETH_USDC), Coin::new(90_000, COSMOS_USDC)]) + pool.exit_pool(&[coin(90_000, ETH_USDC), coin(90_000, COSMOS_USDC)]) .unwrap(); assert_eq!( pool, TransmuterPool { pool_assets: Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(0, ETH_USDC), - Coin::new(0, COSMOS_USDC), + coin(0, ETH_USDC), + coin(0, COSMOS_USDC), ]), asset_groups: BTreeMap::new(), } @@ -107,14 +109,14 @@ mod tests { fn test_exit_pool_fail_when_token_denom_is_invalid() { let mut pool = TransmuterPool { pool_assets: Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(100_000, ETH_USDC), - Coin::new(100_000, COSMOS_USDC), + coin(100_000, ETH_USDC), + coin(100_000, COSMOS_USDC), ]), asset_groups: BTreeMap::new(), }; // exit pool with invalid token - let err = pool.exit_pool(&[Coin::new(10_000, "invalid")]).unwrap_err(); + let err = pool.exit_pool(&[coin(10_000, "invalid")]).unwrap_err(); assert_eq!( err, ContractError::InvalidPoolAssetDenom { @@ -124,7 +126,7 @@ mod tests { // exit pool with both valid and invalid token let err = pool - .exit_pool(&[Coin::new(10_000, ETH_USDC), Coin::new(10_000, "invalid2")]) + .exit_pool(&[coin(10_000, ETH_USDC), coin(10_000, "invalid2")]) .unwrap_err(); assert_eq!( err, @@ -138,45 +140,40 @@ mod tests { fn test_exit_pool_fail_when_not_enough_token() { let mut pool = TransmuterPool { pool_assets: Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(100_000, ETH_USDC), - Coin::new(100_000, COSMOS_USDC), + coin(100_000, ETH_USDC), + coin(100_000, COSMOS_USDC), ]), asset_groups: BTreeMap::new(), }; - let err = pool.exit_pool(&[Coin::new(100_001, ETH_USDC)]).unwrap_err(); + let err = pool.exit_pool(&[coin(100_001, ETH_USDC)]).unwrap_err(); assert_eq!( err, ContractError::InsufficientPoolAsset { - required: Coin::new(100_001, ETH_USDC), - available: Coin::new(100_000, ETH_USDC) + required: coin(100_001, ETH_USDC), + available: coin(100_000, ETH_USDC) } ); - let err = pool - .exit_pool(&[Coin::new(110_000, COSMOS_USDC)]) - .unwrap_err(); + let err = pool.exit_pool(&[coin(110_000, COSMOS_USDC)]).unwrap_err(); assert_eq!( err, ContractError::InsufficientPoolAsset { - required: Coin::new(110_000, COSMOS_USDC), - available: Coin::new(100_000, COSMOS_USDC) + required: coin(110_000, COSMOS_USDC), + available: coin(100_000, COSMOS_USDC) } ); let err = pool - .exit_pool(&[ - Coin::new(110_000, ETH_USDC), - Coin::new(110_000, COSMOS_USDC), - ]) + .exit_pool(&[coin(110_000, ETH_USDC), coin(110_000, COSMOS_USDC)]) .unwrap_err(); assert_eq!( err, ContractError::InsufficientPoolAsset { - required: Coin::new(110_000, ETH_USDC), - available: Coin::new(100_000, ETH_USDC) + required: coin(110_000, ETH_USDC), + available: coin(100_000, ETH_USDC) } ); } diff --git a/contracts/transmuter/src/transmuter_pool/join_pool.rs b/contracts/transmuter/src/transmuter_pool/join_pool.rs index 68a702c..0e4ffe3 100644 --- a/contracts/transmuter/src/transmuter_pool/join_pool.rs +++ b/contracts/transmuter/src/transmuter_pool/join_pool.rs @@ -43,7 +43,7 @@ impl TransmuterPool { #[cfg(test)] mod tests { - use cosmwasm_std::{OverflowError, OverflowOperation}; + use cosmwasm_std::{coin, OverflowError, OverflowOperation}; use crate::asset::Asset; @@ -57,33 +57,30 @@ mod tests { TransmuterPool::new(Asset::unchecked_equal_assets(&[ETH_USDC, COSMOS_USDC])).unwrap(); // join pool - pool.join_pool(&[Coin::new(1000, COSMOS_USDC)]).unwrap(); + pool.join_pool(&[coin(1000, COSMOS_USDC)]).unwrap(); assert_eq!( pool.pool_assets, - Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(0, ETH_USDC), - Coin::new(1000, COSMOS_USDC) - ]) + Asset::unchecked_equal_assets_from_coins(&[coin(0, ETH_USDC), coin(1000, COSMOS_USDC)]) ); // join pool when not empty - pool.join_pool(&[Coin::new(20000, COSMOS_USDC)]).unwrap(); + pool.join_pool(&[coin(20000, COSMOS_USDC)]).unwrap(); assert_eq!( pool.pool_assets, Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(0, ETH_USDC), - Coin::new(21000, COSMOS_USDC) + coin(0, ETH_USDC), + coin(21000, COSMOS_USDC) ]) ); // join pool multiple tokens at once - pool.join_pool(&[Coin::new(1000, ETH_USDC), Coin::new(1000, COSMOS_USDC)]) + pool.join_pool(&[coin(1000, ETH_USDC), coin(1000, COSMOS_USDC)]) .unwrap(); assert_eq!( pool.pool_assets, Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(1000, ETH_USDC), - Coin::new(22000, COSMOS_USDC) + coin(1000, ETH_USDC), + coin(22000, COSMOS_USDC) ]) ); } @@ -94,7 +91,7 @@ mod tests { TransmuterPool::new(Asset::unchecked_equal_assets(&[ETH_USDC, COSMOS_USDC])).unwrap(); assert_eq!( - pool.join_pool(&[Coin::new(1000, "urandom")]).unwrap_err(), + pool.join_pool(&[coin(1000, "urandom")]).unwrap_err(), ContractError::InvalidJoinPoolDenom { denom: "urandom".to_string(), expected_denom: vec![ETH_USDC.to_string(), COSMOS_USDC.to_string()] @@ -103,7 +100,7 @@ mod tests { ); assert_eq!( - pool.join_pool(&[Coin::new(1000, "urandom"), Coin::new(10000, COSMOS_USDC)]) + pool.join_pool(&[coin(1000, "urandom"), coin(10000, COSMOS_USDC)]) .unwrap_err(), ContractError::InvalidJoinPoolDenom { denom: "urandom".to_string(), @@ -120,17 +117,12 @@ mod tests { assert_eq!( { - pool.join_pool(&[Coin::new(1, COSMOS_USDC)]).unwrap(); - pool.join_pool(&[Coin::new(u128::MAX, COSMOS_USDC)]) - .unwrap_err() + pool.join_pool(&[coin(1, COSMOS_USDC)]).unwrap(); + pool.join_pool(&[coin(u128::MAX, COSMOS_USDC)]).unwrap_err() }, - ContractError::Std(StdError::Overflow { - source: OverflowError { - operation: OverflowOperation::Add, - operand1: 1.to_string(), - operand2: u128::MAX.to_string() - } - }), + ContractError::Std(StdError::overflow(OverflowError::new( + OverflowOperation::Add + ))), "join pool overflow" ); } diff --git a/contracts/transmuter/src/transmuter_pool/transmute.rs b/contracts/transmuter/src/transmuter_pool/transmute.rs index 37ec142..5db272b 100644 --- a/contracts/transmuter/src/transmuter_pool/transmute.rs +++ b/contracts/transmuter/src/transmuter_pool/transmute.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{ensure, Coin, Uint128}; +use cosmwasm_std::{coin, ensure, Coin, Uint128}; use crate::{ asset::{convert_amount, Asset, Rounding}, @@ -57,8 +57,8 @@ impl TransmuterPool { &amount_constraint, )?; - let token_in = Coin::new(token_in_amount.u128(), token_in_denom); - let token_out = Coin::new(token_out_amount.u128(), token_out_denom); + let token_in = coin(token_in_amount.u128(), token_in_denom); + let token_out = coin(token_out_amount.u128(), token_out_denom); // ensure there is enough token_out_denom in the pool ensure!( @@ -150,7 +150,7 @@ impl TransmuterPool { #[cfg(test)] mod tests { - use cosmwasm_std::{testing::mock_dependencies, Uint128}; + use cosmwasm_std::{coin, testing::mock_dependencies, Uint128}; use crate::asset::{Asset, AssetConfig}; @@ -166,7 +166,7 @@ mod tests { let mut pool = TransmuterPool::new(Asset::unchecked_equal_assets(&[ETH_USDC, COSMOS_USDC])).unwrap(); - pool.join_pool(&[Coin::new(70_000, COSMOS_USDC)]).unwrap(); + pool.join_pool(&[coin(70_000, COSMOS_USDC)]).unwrap(); assert_eq!( pool.transmute( AmountConstraint::exact_in(70_000u128), @@ -174,10 +174,10 @@ mod tests { COSMOS_USDC ) .unwrap(), - (Coin::new(70_000, ETH_USDC), Coin::new(70_000, COSMOS_USDC)) + (coin(70_000, ETH_USDC), coin(70_000, COSMOS_USDC)) ); - pool.join_pool(&[Coin::new(100_000, COSMOS_USDC)]).unwrap(); + pool.join_pool(&[coin(100_000, COSMOS_USDC)]).unwrap(); assert_eq!( pool.transmute( AmountConstraint::exact_in(60_000u128), @@ -185,7 +185,7 @@ mod tests { COSMOS_USDC ) .unwrap(), - (Coin::new(60_000, ETH_USDC), Coin::new(60_000, COSMOS_USDC)) + (coin(60_000, ETH_USDC), coin(60_000, COSMOS_USDC)) ); assert_eq!( pool.transmute( @@ -194,7 +194,7 @@ mod tests { COSMOS_USDC ) .unwrap(), - (Coin::new(20_000, ETH_USDC), Coin::new(20_000, COSMOS_USDC)) + (coin(20_000, ETH_USDC), coin(20_000, COSMOS_USDC)) ); assert_eq!( pool.transmute( @@ -203,19 +203,19 @@ mod tests { COSMOS_USDC ) .unwrap(), - (Coin::new(20_000, ETH_USDC), Coin::new(20_000, COSMOS_USDC)) + (coin(20_000, ETH_USDC), coin(20_000, COSMOS_USDC)) ); assert_eq!( pool.transmute(AmountConstraint::exact_in(0u128), ETH_USDC, COSMOS_USDC) .unwrap(), - (Coin::new(0, ETH_USDC), Coin::new(0, COSMOS_USDC)) + (coin(0, ETH_USDC), coin(0, COSMOS_USDC)) ); assert_eq!( pool.pool_assets, Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(170_000, ETH_USDC), - Coin::new(0, COSMOS_USDC), + coin(170_000, ETH_USDC), + coin(0, COSMOS_USDC), ]) ); @@ -226,17 +226,14 @@ mod tests { ETH_USDC ) .unwrap(), - ( - Coin::new(100_000, COSMOS_USDC), - Coin::new(100_000, ETH_USDC) - ) + (coin(100_000, COSMOS_USDC), coin(100_000, ETH_USDC)) ); assert_eq!( pool.pool_assets, Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(70_000, ETH_USDC), - Coin::new(100_000, COSMOS_USDC) + coin(70_000, ETH_USDC), + coin(100_000, COSMOS_USDC) ]) ); } @@ -246,7 +243,7 @@ mod tests { let mut pool = TransmuterPool::new(Asset::unchecked_equal_assets(&[ETH_USDC, COSMOS_USDC])).unwrap(); - pool.join_pool(&[Coin::new(170_000, COSMOS_USDC)]).unwrap(); + pool.join_pool(&[coin(170_000, COSMOS_USDC)]).unwrap(); assert_eq!( pool.transmute( @@ -255,20 +252,20 @@ mod tests { COSMOS_USDC ) .unwrap(), - (Coin::new(70_000, ETH_USDC), Coin::new(70_000, COSMOS_USDC)) + (coin(70_000, ETH_USDC), coin(70_000, COSMOS_USDC)) ); assert_eq!( pool.transmute(AmountConstraint::exact_out(0u128), ETH_USDC, COSMOS_USDC) .unwrap(), - (Coin::new(0, ETH_USDC), Coin::new(0, COSMOS_USDC)) + (coin(0, ETH_USDC), coin(0, COSMOS_USDC)) ); assert_eq!( pool.pool_assets, Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(70_000, ETH_USDC), - Coin::new(100_000, COSMOS_USDC), + coin(70_000, ETH_USDC), + coin(100_000, COSMOS_USDC), ]) ); } @@ -278,7 +275,7 @@ mod tests { let mut pool = TransmuterPool::new(Asset::unchecked_equal_assets(&[ETH_USDC, COSMOS_USDC])).unwrap(); - pool.join_pool(&[Coin::new(70_000, COSMOS_USDC)]).unwrap(); + pool.join_pool(&[coin(70_000, COSMOS_USDC)]).unwrap(); assert_eq!( pool.transmute( @@ -287,10 +284,7 @@ mod tests { COSMOS_USDC ) .unwrap(), - ( - Coin::new(70_000, COSMOS_USDC), - Coin::new(70_000, COSMOS_USDC) - ) + (coin(70_000, COSMOS_USDC), coin(70_000, COSMOS_USDC)) ); } @@ -299,7 +293,7 @@ mod tests { let mut pool = TransmuterPool::new(Asset::unchecked_equal_assets(&[ETH_USDC, COSMOS_USDC])).unwrap(); - pool.join_pool(&[Coin::new(70_000, COSMOS_USDC)]).unwrap(); + pool.join_pool(&[coin(70_000, COSMOS_USDC)]).unwrap(); assert_eq!( pool.transmute( AmountConstraint::exact_in(70_001u128), @@ -308,8 +302,8 @@ mod tests { ) .unwrap_err(), ContractError::InsufficientPoolAsset { - required: Coin::new(70_001, COSMOS_USDC), - available: Coin::new(70_000, COSMOS_USDC) + required: coin(70_001, COSMOS_USDC), + available: coin(70_000, COSMOS_USDC) } ); } @@ -319,7 +313,7 @@ mod tests { let mut pool = TransmuterPool::new(Asset::unchecked_equal_assets(&[ETH_USDC, COSMOS_USDC])).unwrap(); - pool.join_pool(&[Coin::new(70_000, COSMOS_USDC)]).unwrap(); + pool.join_pool(&[coin(70_000, COSMOS_USDC)]).unwrap(); assert_eq!( pool.transmute( AmountConstraint::exact_in(70_000u128), @@ -339,7 +333,7 @@ mod tests { let mut pool = TransmuterPool::new(Asset::unchecked_equal_assets(&[ETH_USDC, COSMOS_USDC])).unwrap(); - pool.join_pool(&[Coin::new(70_000, COSMOS_USDC)]).unwrap(); + pool.join_pool(&[coin(70_000, COSMOS_USDC)]).unwrap(); assert_eq!( pool.transmute( AmountConstraint::exact_in(70_000u128), @@ -357,11 +351,11 @@ mod tests { #[test] fn test_transmute_with_normalization_factor_10_power_n() { let mut deps = mock_dependencies(); - deps.querier.update_balance( + deps.querier.bank.update_balance( "creator", vec![ - Coin::new(70_000 * 10u128.pow(14), NBTC_SAT), - Coin::new(70_000 * 10u128.pow(8), WBTC_SAT), + coin(70_000 * 10u128.pow(14), NBTC_SAT), + coin(70_000 * 10u128.pow(8), WBTC_SAT), ], ); @@ -381,7 +375,7 @@ mod tests { ]) .unwrap(); - pool.join_pool(&[Coin::new(70_000 * 10u128.pow(14), NBTC_SAT)]) + pool.join_pool(&[coin(70_000 * 10u128.pow(14), NBTC_SAT)]) .unwrap(); assert_eq!( @@ -392,8 +386,8 @@ mod tests { ) .unwrap(), ( - Coin::new(70_000 * 10u128.pow(8), WBTC_SAT), - Coin::new(70_000 * 10u128.pow(14), NBTC_SAT) + coin(70_000 * 10u128.pow(8), WBTC_SAT), + coin(70_000 * 10u128.pow(14), NBTC_SAT) ) ); @@ -412,11 +406,11 @@ mod tests { fn test_transmute_exact_in_round_down_token_out() { let mut deps = mock_dependencies(); // a:b = 1:3 - deps.querier.update_balance( + deps.querier.bank.update_balance( "creator", vec![ - Coin::new(70_000 * 3 * 10u128.pow(14), "ua"), - Coin::new(70_000 * 10u128.pow(8), "ub"), + coin(70_000 * 3 * 10u128.pow(14), "ua"), + coin(70_000 * 10u128.pow(8), "ub"), ], ); @@ -436,7 +430,7 @@ mod tests { ]) .unwrap(); - pool.join_pool(&[Coin::new(70_000 * 10u128.pow(8), "ub")]) + pool.join_pool(&[coin(70_000 * 10u128.pow(8), "ub")]) .unwrap(); // Transmute with ExactIn, where the output needs to be rounded down @@ -452,8 +446,8 @@ mod tests { assert_eq!( result, ( - Coin::new(3 * 10u128.pow(14) + 1, "ua"), - Coin::new(10u128.pow(8), "ub") + coin(3 * 10u128.pow(14) + 1, "ua"), + coin(10u128.pow(8), "ub") ) ); @@ -469,8 +463,8 @@ mod tests { assert_eq!( result, ( - Coin::new(3 * 10u128.pow(14) - 1, "ua"), - Coin::new(10u128.pow(8) - 1, "ub") + coin(3 * 10u128.pow(14) - 1, "ua"), + coin(10u128.pow(8) - 1, "ub") ) ); } @@ -479,11 +473,11 @@ mod tests { fn test_transmute_exact_out_round_up_token_in() { let mut deps = mock_dependencies(); // a:b = 1:3 - deps.querier.update_balance( + deps.querier.bank.update_balance( "creator", vec![ - Coin::new(70_000 * 3 * 10u128.pow(14), "ua"), - Coin::new(70_000 * 10u128.pow(8), "ub"), + coin(70_000 * 3 * 10u128.pow(14), "ua"), + coin(70_000 * 10u128.pow(8), "ub"), ], ); @@ -503,7 +497,7 @@ mod tests { ]) .unwrap(); - pool.join_pool(&[Coin::new(70_000 * 3 * 10u128.pow(14), "ua")]) + pool.join_pool(&[coin(70_000 * 3 * 10u128.pow(14), "ua")]) .unwrap(); // Transmute with ExactOut, where the input needs to be rounded up @@ -519,8 +513,8 @@ mod tests { assert_eq!( result, ( - Coin::new(10u128.pow(8), "ub"), - Coin::new(3 * 10u128.pow(14) - 1, "ua") + coin(10u128.pow(8), "ub"), + coin(3 * 10u128.pow(14) - 1, "ua") ) ); @@ -547,8 +541,8 @@ mod tests { assert_eq!( result, ( - Coin::new(10u128.pow(8) + 1, "ub"), - Coin::new(3 * 10u128.pow(14) + 1, "ua") + coin(10u128.pow(8) + 1, "ub"), + coin(3 * 10u128.pow(14) + 1, "ua") ) ); diff --git a/contracts/transmuter/src/transmuter_pool/weight.rs b/contracts/transmuter/src/transmuter_pool/weight.rs index b65b514..8cb16d2 100644 --- a/contracts/transmuter/src/transmuter_pool/weight.rs +++ b/contracts/transmuter/src/transmuter_pool/weight.rs @@ -74,7 +74,7 @@ impl TransmuterPool { #[cfg(test)] mod tests { use crate::asset::Asset; - use cosmwasm_std::Coin; + use cosmwasm_std::coin; use rstest::rstest; use std::str::FromStr; @@ -180,8 +180,8 @@ mod tests { fn test_all_ratios_when_total_pool_assets_is_zero() { let pool = TransmuterPool { pool_assets: Asset::unchecked_equal_assets_from_coins(&[ - Coin::new(0, "axlusdc"), - Coin::new(0, "whusdc"), + coin(0, "axlusdc"), + coin(0, "whusdc"), ]), asset_groups: BTreeMap::new(), }; From da1bf54e13b29f20c0397d936cadacb1276ff436 Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Wed, 23 Oct 2024 10:21:26 +0700 Subject: [PATCH 20/26] update test-tube version --- Cargo.lock | 4 ++++ contracts/transmuter/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index ce50a47..d7db25e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1559,6 +1559,8 @@ dependencies = [ [[package]] name = "osmosis-test-tube" version = "26.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bb569512b10f8fb1fa4fe2e2dc0b474f1fca7ed80500a4328593fc85ad0c3e6" dependencies = [ "base64 0.22.1", "bindgen", @@ -2559,6 +2561,8 @@ dependencies = [ [[package]] name = "test-tube" version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc02a1036b1d92445cb09078bcc933a84937586e6acb28ee5dfa7b5c20be569f" dependencies = [ "base64 0.22.1", "cosmrs", diff --git a/contracts/transmuter/Cargo.toml b/contracts/transmuter/Cargo.toml index e64bf21..ecc62fc 100644 --- a/contracts/transmuter/Cargo.toml +++ b/contracts/transmuter/Cargo.toml @@ -55,7 +55,7 @@ transmuter_math = {version = "1.0.0", path = "../../packages/transmuter_math"} [dev-dependencies] itertools = "0.13.0" -osmosis-test-tube = {path = "../../../test-tube/packages/osmosis-test-tube"} +osmosis-test-tube = "26.0.1" rstest = "0.23.0" [lints.rust] From 72a8c39a1d1bd018f82248fee0f1605c5ff11427 Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Thu, 24 Oct 2024 18:07:12 +0700 Subject: [PATCH 21/26] fix create_asset_group comment --- contracts/transmuter/src/contract.rs | 1 - contracts/transmuter/src/error.rs | 3 +++ .../src/transmuter_pool/asset_group.rs | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/contracts/transmuter/src/contract.rs b/contracts/transmuter/src/contract.rs index 4b035e2..8158952 100644 --- a/contracts/transmuter/src/contract.rs +++ b/contracts/transmuter/src/contract.rs @@ -1974,7 +1974,6 @@ mod tests { fn test_corrupted_asset_group() { let mut deps = mock_dependencies(); let admin = deps.api.addr_make("admin"); - let moderator = deps.api.addr_make("moderator"); let user = deps.api.addr_make("user"); let moderator = deps.api.addr_make("moderator"); diff --git a/contracts/transmuter/src/error.rs b/contracts/transmuter/src/error.rs index 56c3325..dbe932b 100644 --- a/contracts/transmuter/src/error.rs +++ b/contracts/transmuter/src/error.rs @@ -191,6 +191,9 @@ pub enum ContractError { #[error("Asset group {label} already exists")] AssetGroupAlreadyExists { label: String }, + #[error("Asset group label must not be empty")] + EmptyAssetGroupLabel {}, + #[error("{0}")] OverflowError(#[from] OverflowError), diff --git a/contracts/transmuter/src/transmuter_pool/asset_group.rs b/contracts/transmuter/src/transmuter_pool/asset_group.rs index 921ab05..bddb4ff 100644 --- a/contracts/transmuter/src/transmuter_pool/asset_group.rs +++ b/contracts/transmuter/src/transmuter_pool/asset_group.rs @@ -99,6 +99,9 @@ impl TransmuterPool { } ); + // ensure that asset group label is not empty string + ensure!(!label.is_empty(), ContractError::EmptyAssetGroupLabel {}); + // ensure that all denoms are valid pool assets and has no duplicated denoms // ensuring no duplicated denoms also ensures that it's within MAX_POOL_ASSET_DENOMS limit let mut denoms_set = HashSet::new(); @@ -251,6 +254,21 @@ mod tests { ); } + #[test] + fn test_create_asset_group_with_empty_string() { + let mut pool = TransmuterPool::new(vec![ + Asset::new(Uint128::new(100), "denom1", Uint128::new(1)).unwrap(), + Asset::new(Uint128::new(200), "denom2", Uint128::new(1)).unwrap(), + ]) + .unwrap(); + + let err = pool + .create_asset_group("".to_string(), vec!["denom1".to_string()]) + .unwrap_err(); + + assert_eq!(err, ContractError::EmptyAssetGroupLabel {}); + } + #[test] fn test_create_asset_group_with_duplicated_denom() { let mut pool = TransmuterPool::new(vec![ From be7ba557618ba04c4e19719520ba6fe5d3fc59d8 Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Thu, 24 Oct 2024 18:12:47 +0700 Subject: [PATCH 22/26] remove unused imports --- contracts/transmuter/src/sudo.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/transmuter/src/sudo.rs b/contracts/transmuter/src/sudo.rs index 74319bb..904a2ac 100644 --- a/contracts/transmuter/src/sudo.rs +++ b/contracts/transmuter/src/sudo.rs @@ -177,7 +177,7 @@ mod tests { }; use cosmwasm_std::{ coin, - testing::{message_info, mock_dependencies, mock_env, mock_info, MOCK_CONTRACT_ADDR}, + testing::{message_info, mock_dependencies, mock_env, MOCK_CONTRACT_ADDR}, to_json_binary, BankMsg, Binary, Reply, SubMsgResponse, SubMsgResult, }; use osmosis_std::types::osmosis::tokenfactory::v1beta1::{ From ba1e1610b6948e7c6b52cd1ed6e0d39fcd3a2886 Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Thu, 24 Oct 2024 18:30:15 +0700 Subject: [PATCH 23/26] fix deprecataed --- contracts/transmuter/src/contract.rs | 228 ++++-------------- contracts/transmuter/src/sudo.rs | 57 ++--- .../src/test/cases/units/spot_price.rs | 5 +- 3 files changed, 80 insertions(+), 210 deletions(-) diff --git a/contracts/transmuter/src/contract.rs b/contracts/transmuter/src/contract.rs index 8158952..0113c42 100644 --- a/contracts/transmuter/src/contract.rs +++ b/contracts/transmuter/src/contract.rs @@ -1032,8 +1032,8 @@ mod tests { use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env}; use cosmwasm_std::{ - attr, coin, from_json, BankMsg, Binary, BlockInfo, Storage, SubMsgResponse, SubMsgResult, - Uint64, + attr, coin, from_json, BankMsg, Binary, BlockInfo, MsgResponse, Storage, SubMsgResponse, + SubMsgResult, Uint64, }; use osmosis_std::types::osmosis::tokenfactory::v1beta1::MsgBurn; @@ -1110,21 +1110,26 @@ mod tests { // Manually reply let alloyed_denom = "usomoion"; + let msg_create_denom_response = MsgCreateDenomResponse { + new_token_denom: alloyed_denom.to_string(), + }; + reply( deps.as_mut(), env.clone(), Reply { id: 1, - result: SubMsgResult::Ok(SubMsgResponse { - events: vec![], - data: Some( - MsgCreateDenomResponse { - new_token_denom: alloyed_denom.to_string(), - } - .into(), - ), - msg_responses: vec![], - }), + result: SubMsgResult::Ok( + #[allow(deprecated)] + SubMsgResponse { + events: vec![], + data: Some(msg_create_denom_response.clone().into()), // DEPRECATED + msg_responses: vec![MsgResponse { + type_url: MsgCreateDenomResponse::TYPE_URL.to_string(), + value: msg_create_denom_response.into(), + }], + }, + ), payload: Binary::new(vec![]), gas_used: 0, }, @@ -1389,21 +1394,7 @@ mod tests { let res = reply( deps.as_mut(), env.clone(), - Reply { - id: 1, - result: SubMsgResult::Ok(SubMsgResponse { - events: vec![], - data: Some( - MsgCreateDenomResponse { - new_token_denom: alloyed_subdenom.to_string(), - } - .into(), - ), - msg_responses: vec![], - }), - payload: Binary::new(vec![]), - gas_used: 0, - }, + reply_create_denom_response(alloyed_subdenom), ) .unwrap(); @@ -2009,21 +2000,7 @@ mod tests { let res = reply( deps.as_mut(), env.clone(), - Reply { - id: 1, - result: SubMsgResult::Ok(SubMsgResponse { - events: vec![], - data: Some( - MsgCreateDenomResponse { - new_token_denom: "btc".to_string(), - } - .into(), - ), - msg_responses: vec![], - }), - payload: Binary::new(vec![]), - gas_used: 0, - }, + reply_create_denom_response("btc"), ) .unwrap(); @@ -3230,21 +3207,7 @@ mod tests { reply( deps.as_mut(), env.clone(), - Reply { - id: 1, - result: SubMsgResult::Ok(SubMsgResponse { - events: vec![], - data: Some( - MsgCreateDenomResponse { - new_token_denom: alloyed_denom.to_string(), - } - .into(), - ), - msg_responses: vec![], - }), - payload: Binary::new(vec![]), - gas_used: 0, - }, + reply_create_denom_response(alloyed_denom), ) .unwrap(); @@ -3312,21 +3275,7 @@ mod tests { reply( deps.as_mut(), env.clone(), - Reply { - id: 1, - result: SubMsgResult::Ok(SubMsgResponse { - events: vec![], - data: Some( - MsgCreateDenomResponse { - new_token_denom: alloyed_denom.to_string(), - } - .into(), - ), - msg_responses: vec![], - }), - payload: Binary::new(vec![]), - gas_used: 0, - }, + reply_create_denom_response(alloyed_denom), ) .unwrap(); @@ -3444,21 +3393,7 @@ mod tests { reply( deps.as_mut(), env.clone(), - Reply { - id: 1, - result: SubMsgResult::Ok(SubMsgResponse { - events: vec![], - data: Some( - MsgCreateDenomResponse { - new_token_denom: alloyed_denom.to_string(), - } - .into(), - ), - msg_responses: vec![], - }), - payload: Binary::new(vec![]), - gas_used: 0, - }, + reply_create_denom_response(alloyed_denom), ) .unwrap(); @@ -3588,21 +3523,7 @@ mod tests { reply( deps.as_mut(), env.clone(), - Reply { - id: 1, - result: SubMsgResult::Ok(SubMsgResponse { - events: vec![], - data: Some( - MsgCreateDenomResponse { - new_token_denom: alloyed_denom.to_string(), - } - .into(), - ), - msg_responses: vec![], - }), - payload: Binary::new(vec![]), - gas_used: 0, - }, + reply_create_denom_response(alloyed_denom), ) .unwrap(); @@ -3653,21 +3574,7 @@ mod tests { reply( deps.as_mut(), env.clone(), - Reply { - id: 1, - result: SubMsgResult::Ok(SubMsgResponse { - events: vec![], - data: Some( - MsgCreateDenomResponse { - new_token_denom: alloyed_denom.to_string(), - } - .into(), - ), - msg_responses: vec![], - }), - payload: Binary::new(vec![]), - gas_used: 0, - }, + reply_create_denom_response(alloyed_denom), ) .unwrap(); @@ -3806,21 +3713,7 @@ mod tests { reply( deps.as_mut(), env.clone(), - Reply { - id: 1, - result: SubMsgResult::Ok(SubMsgResponse { - events: vec![], - data: Some( - MsgCreateDenomResponse { - new_token_denom: alloyed_denom.to_string(), - } - .into(), - ), - msg_responses: vec![], - }), - payload: Binary::new(vec![]), - gas_used: 0, - }, + reply_create_denom_response(alloyed_denom), ) .unwrap(); @@ -3921,21 +3814,7 @@ mod tests { reply( deps.as_mut(), env.clone(), - Reply { - id: 1, - result: SubMsgResult::Ok(SubMsgResponse { - events: vec![], - data: Some( - MsgCreateDenomResponse { - new_token_denom: alloyed_denom.to_string(), - } - .into(), - ), - msg_responses: vec![], - }), - payload: Binary::new(vec![]), - gas_used: 0, - }, + reply_create_denom_response(alloyed_denom), ) .unwrap(); @@ -4136,21 +4015,7 @@ mod tests { reply( deps.as_mut(), env.clone(), - Reply { - id: 1, - result: SubMsgResult::Ok(SubMsgResponse { - events: vec![], - data: Some( - MsgCreateDenomResponse { - new_token_denom: alloyed_denom.to_string(), - } - .into(), - ), - msg_responses: vec![], - }), - payload: Binary::new(vec![]), - gas_used: 0, - }, + reply_create_denom_response(alloyed_denom), ) .unwrap(); @@ -4342,21 +4207,7 @@ mod tests { reply( deps.as_mut(), env.clone(), - Reply { - id: 1, - result: SubMsgResult::Ok(SubMsgResponse { - events: vec![], - data: Some( - MsgCreateDenomResponse { - new_token_denom: alloyed_denom.to_string(), - } - .into(), - ), - msg_responses: vec![], - }), - payload: Binary::new(vec![]), - gas_used: 0, - }, + reply_create_denom_response(alloyed_denom), ) .unwrap(); @@ -4938,4 +4789,27 @@ mod tests { // Check that the asset group is no longer in the corrupted scopes list assert_eq!(query_res.corrupted_scopes, vec![Scope::denom("asset1")]); } + + fn reply_create_denom_response(alloyed_denom: &str) -> Reply { + let msg_create_denom_response = MsgCreateDenomResponse { + new_token_denom: alloyed_denom.to_string(), + }; + + Reply { + id: 1, + result: SubMsgResult::Ok( + #[allow(deprecated)] + SubMsgResponse { + events: vec![], + data: Some(msg_create_denom_response.clone().into()), // DEPRECATED + msg_responses: vec![MsgResponse { + type_url: MsgCreateDenomResponse::TYPE_URL.to_string(), + value: msg_create_denom_response.into(), + }], + }, + ), + payload: Binary::new(vec![]), + gas_used: 0, + } + } } diff --git a/contracts/transmuter/src/sudo.rs b/contracts/transmuter/src/sudo.rs index 904a2ac..3609cd8 100644 --- a/contracts/transmuter/src/sudo.rs +++ b/contracts/transmuter/src/sudo.rs @@ -178,7 +178,7 @@ mod tests { use cosmwasm_std::{ coin, testing::{message_info, mock_dependencies, mock_env, MOCK_CONTRACT_ADDR}, - to_json_binary, BankMsg, Binary, Reply, SubMsgResponse, SubMsgResult, + to_json_binary, BankMsg, Binary, MsgResponse, Reply, SubMsgResponse, SubMsgResult, }; use osmosis_std::types::osmosis::tokenfactory::v1beta1::{ MsgBurn, MsgCreateDenomResponse, MsgMint, @@ -219,21 +219,7 @@ mod tests { reply( deps.as_mut(), env.clone(), - Reply { - id: 1, - result: SubMsgResult::Ok(SubMsgResponse { - events: vec![], - data: Some( - MsgCreateDenomResponse { - new_token_denom: alloyed_denom.to_string(), - } - .into(), - ), - msg_responses: vec![], - }), - payload: Binary::new(vec![]), - gas_used: 0, - }, + reply_create_denom_response(alloyed_denom), ) .unwrap(); @@ -444,21 +430,7 @@ mod tests { reply( deps.as_mut(), env.clone(), - Reply { - id: 1, - result: SubMsgResult::Ok(SubMsgResponse { - events: vec![], - data: Some( - MsgCreateDenomResponse { - new_token_denom: alloyed_denom.to_string(), - } - .into(), - ), - msg_responses: vec![], - }), - payload: Binary::new(vec![]), - gas_used: 0, - }, + reply_create_denom_response(alloyed_denom), ) .unwrap(); @@ -634,4 +606,27 @@ mod tests { }) ); } + + fn reply_create_denom_response(alloyed_denom: &str) -> Reply { + let msg_create_denom_response = MsgCreateDenomResponse { + new_token_denom: alloyed_denom.to_string(), + }; + + Reply { + id: 1, + result: SubMsgResult::Ok( + #[allow(deprecated)] + SubMsgResponse { + events: vec![], + data: Some(msg_create_denom_response.clone().into()), // DEPRECATED + msg_responses: vec![MsgResponse { + type_url: MsgCreateDenomResponse::TYPE_URL.to_string(), + value: msg_create_denom_response.into(), + }], + }, + ), + payload: Binary::new(vec![]), + gas_used: 0, + } + } } diff --git a/contracts/transmuter/src/test/cases/units/spot_price.rs b/contracts/transmuter/src/test/cases/units/spot_price.rs index 3bb9934..635eeb1 100644 --- a/contracts/transmuter/src/test/cases/units/spot_price.rs +++ b/contracts/transmuter/src/test/cases/units/spot_price.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ coin, - testing::{message_info, mock_dependencies, mock_env, mock_info}, + testing::{message_info, mock_dependencies, mock_env}, Coin, Decimal, Uint128, }; use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx}; @@ -54,11 +54,12 @@ fn test_spot_price(liquidity: &[Coin]) { ) .unwrap(); + let creator = deps.api.addr_make("creator"); transmuter .join_pool(ExecCtx { deps: deps.as_mut(), env: mock_env(), - info: mock_info("creator", liquidity), + info: message_info(&creator, liquidity), }) .unwrap(); From fb4eb674d690e519b50e12d239462ef519309707 Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Thu, 24 Oct 2024 18:36:19 +0700 Subject: [PATCH 24/26] update rustc toolchain in gh workflow --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 28551b2..3df68a4 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -19,7 +19,7 @@ jobs: - name: Install Rust uses: actions-rs/toolchain@v1 with: - toolchain: 1.72.0 + toolchain: 1.83.0 target: wasm32-unknown-unknown profile: minimal override: true From 555108180e9adccfdc45984bed509bf5e1141b91 Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Thu, 24 Oct 2024 18:40:58 +0700 Subject: [PATCH 25/26] update rustc toolchain in gh workflow --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 3df68a4..84339d9 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -19,7 +19,7 @@ jobs: - name: Install Rust uses: actions-rs/toolchain@v1 with: - toolchain: 1.83.0 + toolchain: 1.82.0 target: wasm32-unknown-unknown profile: minimal override: true From 18f7a24cf07a03cd41bc04d07acc677dd618274c Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Thu, 24 Oct 2024 19:46:32 +0700 Subject: [PATCH 26/26] use rust 1.81.1 to avoid wasm build failure --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 84339d9..b1b328a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -19,7 +19,7 @@ jobs: - name: Install Rust uses: actions-rs/toolchain@v1 with: - toolchain: 1.82.0 + toolchain: 1.81.0 target: wasm32-unknown-unknown profile: minimal override: true