From 943f04ef68d79a1cc0f8e452b9011ccfe9f0b194 Mon Sep 17 00:00:00 2001 From: Supanat Potiwarakorn Date: Wed, 8 May 2024 13:35:18 +0700 Subject: [PATCH] calculate price base on normalization factor --- contracts/transmuter/src/contract.rs | 183 ++++++++++++++---- contracts/transmuter/src/math.rs | 58 +++++- .../src/test/cases/units/spot_price.rs | 4 +- 3 files changed, 207 insertions(+), 38 deletions(-) diff --git a/contracts/transmuter/src/contract.rs b/contracts/transmuter/src/contract.rs index 7a743c4..302732a 100644 --- a/contracts/transmuter/src/contract.rs +++ b/contracts/transmuter/src/contract.rs @@ -1,4 +1,4 @@ -use std::iter; +use std::{collections::BTreeMap, iter}; use crate::{ alloyed_asset::AlloyedAsset, @@ -6,7 +6,7 @@ use crate::{ ensure_admin_authority, ensure_moderator_authority, error::{non_empty_input_required, nonpayable, ContractError}, limiter::{Limiter, LimiterParams, Limiters}, - math::rescale, + math::{self, rescale}, role::Role, swap::{BurnTarget, Entrypoint, SwapFromAlloyedConstraint, SwapToAlloyedConstraint, SWAP_FEE}, transmuter_pool::TransmuterPool, @@ -612,8 +612,8 @@ impl Transmuter<'_> { pub(crate) fn spot_price( &self, QueryCtx { deps, env: _ }: QueryCtx, - quote_asset_denom: String, base_asset_denom: String, + quote_asset_denom: String, ) -> Result { // ensure that it's not the same denom ensure!( @@ -626,41 +626,39 @@ impl Transmuter<'_> { // ensure that qoute asset denom are in swappable assets let pool = self.pool.load(deps.storage)?; let alloyed_denom = self.alloyed_asset.get_alloyed_denom(deps.storage)?; - let swappable_assets = pool + let alloyed_normalization_factor = + self.alloyed_asset.get_normalization_factor(deps.storage)?; + let swappable_asset_norm_factors = pool .pool_assets .iter() - .map(|c| c.denom().to_string()) - .chain(vec![alloyed_denom]) - .collect::>(); - - ensure!( - swappable_assets - .iter() - .any(|denom| denom == quote_asset_denom.as_str()), - ContractError::SpotPriceQueryFailed { + .map(|c| (c.denom().to_string(), c.normalization_factor())) + .chain(vec![(alloyed_denom, alloyed_normalization_factor)]) + .collect::>(); + + let base_asset_norm_factor = swappable_asset_norm_factors + .get(&base_asset_denom) + .cloned() + .ok_or_else(|| ContractError::SpotPriceQueryFailed { reason: format!( - "quote_asset_denom is not in swappable assets: must be one of {:?} but got {}", - swappable_assets, quote_asset_denom - ) - } - ); + "base_asset_denom is not in swappable assets: must be one of {:?} but got {}", + swappable_asset_norm_factors.keys(), + base_asset_denom + ), + })?; - // ensure that base asset denom are in swappable assets - ensure!( - swappable_assets - .iter() - .any(|denom| denom == base_asset_denom.as_str()), - ContractError::SpotPriceQueryFailed { + let quote_asset_norm_factor = swappable_asset_norm_factors + .get("e_asset_denom) + .cloned() + .ok_or_else(|| ContractError::SpotPriceQueryFailed { reason: format!( - "base_asset_denom is not in swappable assets: must be one of {:?} but got {}", - swappable_assets, base_asset_denom - ) - } - ); + "quote_asset_denom is not in swappable assets: must be one of {:?} but got {}", + swappable_asset_norm_factors.keys(), + quote_asset_denom + ), + })?; - // spot price is always one for both side Ok(SpotPriceResponse { - spot_price: Decimal::one(), + spot_price: math::price(base_asset_norm_factor, quote_asset_norm_factor)?, }) } @@ -3058,7 +3056,7 @@ mod tests { AssetConfig::from_denom_str("uion"), ], admin: Some(admin.to_string()), - alloyed_asset_subdenom: "usomoion".to_string(), + alloyed_asset_subdenom: "uosmoion".to_string(), alloyed_asset_normalization_factor: Uint128::one(), moderator: "moderator".to_string(), }; @@ -3069,7 +3067,7 @@ mod tests { instantiate(deps.as_mut(), env.clone(), info, init_msg).unwrap(); // Manually reply - let alloyed_denom = "usomoion"; + let alloyed_denom = "uosmoion"; reply( deps.as_mut(), @@ -3121,7 +3119,7 @@ mod tests { assert_eq!( err, ContractError::SpotPriceQueryFailed { - reason: "quote_asset_denom is not in swappable assets: must be one of [\"uosmo\", \"uion\", \"usomoion\"] but got uatom".to_string() + reason: "quote_asset_denom is not in swappable assets: must be one of [\"uion\", \"uosmo\", \"uosmoion\"] but got uatom".to_string() } ); @@ -3138,7 +3136,7 @@ mod tests { assert_eq!( err, ContractError::SpotPriceQueryFailed { - reason: "base_asset_denom is not in swappable assets: must be one of [\"uosmo\", \"uion\", \"usomoion\"] but got uatom".to_string() + reason: "base_asset_denom is not in swappable assets: must be one of [\"uion\", \"uosmo\", \"uosmoion\"] but got uatom".to_string() } ); @@ -3184,6 +3182,121 @@ mod tests { assert_eq!(spot_price.spot_price, Decimal::one()); } + #[test] + fn test_spot_price_with_different_norm_factor() { + let mut deps = mock_dependencies(); + + // make denom has non-zero total supply + deps.querier + .update_balance("someone", vec![Coin::new(1, "tbtc"), Coin::new(1, "nbtc")]); + + let admin = "admin"; + let init_msg = InstantiateMsg { + pool_asset_configs: vec![ + AssetConfig { + denom: "tbtc".to_string(), + normalization_factor: Uint128::from(1u128), + }, + AssetConfig { + denom: "nbtc".to_string(), + normalization_factor: Uint128::from(100u128), + }, + ], + admin: Some(admin.to_string()), + alloyed_asset_subdenom: "allbtc".to_string(), + alloyed_asset_normalization_factor: Uint128::from(100u128), + moderator: "moderator".to_string(), + }; + let env = mock_env(); + let info = mock_info(admin, &[]); + + // Instantiate the contract. + instantiate(deps.as_mut(), env.clone(), info, init_msg).unwrap(); + + // Manually reply + let alloyed_denom = "allbtc"; + + 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(), + ), + }), + }, + ) + .unwrap(); + + // Test spot price with pool assets + let res = query( + deps.as_ref(), + env.clone(), + ContractQueryMsg::Transmuter(QueryMsg::SpotPrice { + base_asset_denom: "nbtc".to_string(), + quote_asset_denom: "tbtc".to_string(), + }), + ) + .unwrap(); + + // tbtc/1 = nbtc/100 + // tbtc = 1nbtc/100 + let spot_price: SpotPriceResponse = from_json(res).unwrap(); + assert_eq!(spot_price.spot_price, Decimal::from_ratio(1u128, 100u128)); + + let res = query( + deps.as_ref(), + env.clone(), + ContractQueryMsg::Transmuter(QueryMsg::SpotPrice { + base_asset_denom: "tbtc".to_string(), + quote_asset_denom: "nbtc".to_string(), + }), + ) + .unwrap(); + + // nbtc/100 = tbtc/1 + // nbtc = 100tbtc + let spot_price: SpotPriceResponse = from_json(res).unwrap(); + assert_eq!(spot_price.spot_price, Decimal::from_ratio(100u128, 1u128)); + + // Test spot price with alloyed denom + let res = query( + deps.as_ref(), + env.clone(), + ContractQueryMsg::Transmuter(QueryMsg::SpotPrice { + quote_asset_denom: "nbtc".to_string(), + base_asset_denom: alloyed_denom.to_string(), + }), + ) + .unwrap(); + + // nbtc/100 = allbtc/100 + // nbtc = 1allbtc + let spot_price: SpotPriceResponse = from_json(res).unwrap(); + assert_eq!(spot_price.spot_price, Decimal::one()); + + let res = query( + deps.as_ref(), + env, + ContractQueryMsg::Transmuter(QueryMsg::SpotPrice { + quote_asset_denom: alloyed_denom.to_string(), + base_asset_denom: "tbtc".to_string(), + }), + ) + .unwrap(); + + // allbtc/100 = tbtc/1 + // tbtc = 100allbtc + let spot_price: SpotPriceResponse = from_json(res).unwrap(); + assert_eq!(spot_price.spot_price, Decimal::from_ratio(100u128, 1u128)); + } + #[test] fn test_calc_out_amt_given_in() { let mut deps = mock_dependencies(); diff --git a/contracts/transmuter/src/math.rs b/contracts/transmuter/src/math.rs index d4ee606..293eda4 100644 --- a/contracts/transmuter/src/math.rs +++ b/contracts/transmuter/src/math.rs @@ -1,4 +1,6 @@ -use cosmwasm_std::{ensure, CheckedMultiplyRatioError, DivideByZeroError, Uint128}; +use cosmwasm_std::{ + ensure, CheckedFromRatioError, CheckedMultiplyRatioError, Decimal, DivideByZeroError, Uint128, +}; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -6,6 +8,9 @@ pub enum MathError { #[error("{0}")] CheckedMultiplyRatioError(#[from] CheckedMultiplyRatioError), + #[error("{0}")] + CheckedFromRatioError(#[from] CheckedFromRatioError), + #[error("{0}")] DivideByZeroError(#[from] DivideByZeroError), @@ -72,6 +77,23 @@ pub fn rescale(n: Uint128, numerator: Uint128, denominator: Uint128) -> MathResu .map_err(Into::into) } +/// Calculate the price of the base asset in terms of the quote asset based on the normalized factors +/// +/// ``` +/// quote_amt / quote_norm_factor = base_amt / base_norm_factor +/// quote_amt = base_amt * quote_norm_factor / base_norm_factor +/// ``` +/// +/// spot price is how much of the quote asset is needed to buy one unit of the base asset +/// therefore: +/// +/// ``` +/// spot_price = 1 * quote_norm_factor / base_norm_factor +/// ``` +pub fn price(base_norm_factor: Uint128, quote_norm_factor: Uint128) -> MathResult { + Decimal::checked_from_ratio(quote_norm_factor, base_norm_factor).map_err(Into::into) +} + #[cfg(test)] mod tests { use super::*; @@ -182,4 +204,38 @@ mod tests { expected.map(Uint128::from) ); } + + #[rstest] + #[case(1u128, 1u128, Ok(Decimal::one()))] + #[case(10u128, 20u128, Ok(Decimal::from_ratio(2u128, 1u128)))] + #[case(100u128, 200u128, Ok(Decimal::from_ratio(2u128, 1u128)))] + #[case( + 10_000_000_000_000_000u128, + 1_000_000_000_000_000_000u128, + Ok(Decimal::from_ratio(100u128, 1u128)) + )] + #[case( + 1_000_000_000_000_000_000u128, + 10_000_000_000_000_000u128, + Ok(Decimal::from_ratio(1u128, 100u128)) + )] + #[case(100u128, 0u128, Ok(Decimal::zero()))] + #[case( + 0u128, + 100u128, + Err(MathError::CheckedFromRatioError(CheckedFromRatioError::DivideByZero)) + )] + fn test_price( + #[case] base_norm_factor: u128, + #[case] quote_norm_factor: u128, + #[case] expected: MathResult, + ) { + assert_eq!( + price( + Uint128::from(base_norm_factor), + Uint128::from(quote_norm_factor) + ), + expected + ); + } } diff --git a/contracts/transmuter/src/test/cases/units/spot_price.rs b/contracts/transmuter/src/test/cases/units/spot_price.rs index 4b9fc3b..c020253 100644 --- a/contracts/transmuter/src/test/cases/units/spot_price.rs +++ b/contracts/transmuter/src/test/cases/units/spot_price.rs @@ -90,7 +90,7 @@ fn test_spot_price(liquidity: &[Coin]) { ) .unwrap_err(), ContractError::SpotPriceQueryFailed { - reason: "quote_asset_denom is not in swappable assets: must be one of [\"denom0\", \"denom1\", \"factory/contract_address/transmuter/poolshare\"] but got random_denom".to_string() + reason: "base_asset_denom is not in swappable assets: must be one of [\"denom0\", \"denom1\", \"factory/contract_address/transmuter/poolshare\"] but got random_denom".to_string() } ); @@ -106,7 +106,7 @@ fn test_spot_price(liquidity: &[Coin]) { ) .unwrap_err(), ContractError::SpotPriceQueryFailed { - reason: "base_asset_denom is not in swappable assets: must be one of [\"denom0\", \"denom1\", \"factory/contract_address/transmuter/poolshare\"] but got random_denom".to_string() + reason: "quote_asset_denom is not in swappable assets: must be one of [\"denom0\", \"denom1\", \"factory/contract_address/transmuter/poolshare\"] but got random_denom".to_string() } );