Skip to content

Commit

Permalink
calculate price base on normalization factor
Browse files Browse the repository at this point in the history
  • Loading branch information
iboss-ptk committed May 8, 2024
1 parent 7064908 commit 943f04e
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 38 deletions.
183 changes: 148 additions & 35 deletions contracts/transmuter/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use std::iter;
use std::{collections::BTreeMap, iter};

use crate::{
alloyed_asset::AlloyedAsset,
asset::{Asset, AssetConfig},
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,
Expand Down Expand Up @@ -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<SpotPriceResponse, ContractError> {
// ensure that it's not the same denom
ensure!(
Expand All @@ -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::<Vec<_>>();

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::<BTreeMap<String, Uint128>>();

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(&quote_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)?,
})
}

Expand Down Expand Up @@ -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(),
};
Expand All @@ -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(),
Expand Down Expand Up @@ -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()
}
);

Expand All @@ -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()
}
);

Expand Down Expand Up @@ -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();
Expand Down
58 changes: 57 additions & 1 deletion contracts/transmuter/src/math.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
use cosmwasm_std::{ensure, CheckedMultiplyRatioError, DivideByZeroError, Uint128};
use cosmwasm_std::{
ensure, CheckedFromRatioError, CheckedMultiplyRatioError, Decimal, DivideByZeroError, Uint128,
};
use thiserror::Error;

#[derive(Error, Debug, PartialEq)]
pub enum MathError {
#[error("{0}")]
CheckedMultiplyRatioError(#[from] CheckedMultiplyRatioError),

#[error("{0}")]
CheckedFromRatioError(#[from] CheckedFromRatioError),

#[error("{0}")]
DivideByZeroError(#[from] DivideByZeroError),

Expand Down Expand Up @@ -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> {
Decimal::checked_from_ratio(quote_norm_factor, base_norm_factor).map_err(Into::into)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -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<Decimal>,
) {
assert_eq!(
price(
Uint128::from(base_norm_factor),
Uint128::from(quote_norm_factor)
),
expected
);
}
}
4 changes: 2 additions & 2 deletions contracts/transmuter/src/test/cases/units/spot_price.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
);

Expand All @@ -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()
}
);

Expand Down

0 comments on commit 943f04e

Please sign in to comment.