abstract_core/objects/
price_source.rs

1//! # Proxy Asset
2//! Proxy assets are objects that describe an asset and a way to calculate that asset's value against a base asset.
3//!
4//! ## Details
5//! A proxy asset is composed of two components.
6//! * The `asset`, which is an [`AssetInfo`].
7//! * The [`PriceSource`] which is an enum that indicates how to calculate the value for that asset.
8//!
9//! The base asset is the asset for which `price_source` in `None`.
10//! **There should only be ONE base asset when configuring your proxy**
11
12use cosmwasm_std::{
13    to_json_binary, Addr, Decimal, Deps, QuerierWrapper, QueryRequest, StdError, Uint128, WasmQuery,
14};
15use cw_asset::{Asset, AssetInfo};
16use schemars::JsonSchema;
17use serde::{Deserialize, Serialize};
18
19use super::{
20    ans_host::AnsHost, AnsEntryConvertor, AssetEntry, DexAssetPairing, PoolAddress, PoolReference,
21};
22use crate::{error::AbstractError, AbstractResult};
23
24/// represents the conversion of an asset in terms of the provided asset
25/// Example: provided asset is ETH and the price source for ETH is the pair ETH/USD, the price is 100USD/ETH
26/// then `AssetConversion { into: USD, ratio: 100}`
27#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
28pub struct AssetConversion {
29    into: AssetInfo,
30    ratio: Decimal,
31}
32
33impl AssetConversion {
34    pub fn new(asset: impl Into<AssetInfo>, price: Decimal) -> Self {
35        Self {
36            into: asset.into(),
37            ratio: price,
38        }
39    }
40    /// convert the balance of an asset into a (list of) asset(s) given the provided rate(s)
41    pub fn convert(rates: &[Self], amount: Uint128) -> Vec<Asset> {
42        rates
43            .iter()
44            .map(|rate| Asset::new(rate.into.clone(), amount * rate.ratio))
45            .collect()
46    }
47}
48
49/// Provides information on how to calculate the value of an asset
50#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
51#[non_exhaustive]
52pub enum UncheckedPriceSource {
53    /// A pool address of an asset/asset pair
54    /// Both assets must be defined in the Proxy_assets state
55    Pair(DexAssetPairing),
56    // Liquidity Pool token
57    LiquidityToken {},
58    // a Proxy, the proxy also takes a Decimal (the multiplier)
59    // Asset will be valued as if they are Proxy tokens
60    ValueAs {
61        asset: AssetEntry,
62        multiplier: Decimal,
63    },
64    None,
65}
66
67impl UncheckedPriceSource {
68    pub fn check(
69        self,
70        deps: Deps,
71        ans_host: &AnsHost,
72        entry: &AssetEntry,
73    ) -> AbstractResult<PriceSource> {
74        match self {
75            UncheckedPriceSource::Pair(pair_info) => {
76                let PoolReference {
77                    pool_address,
78                    unique_id,
79                } = ans_host
80                    .query_asset_pairing(&deps.querier, &pair_info)?
81                    .pop()
82                    .unwrap();
83                let pool_assets = ans_host
84                    .query_pool_metadata(&deps.querier, unique_id)?
85                    .assets;
86                let assets = ans_host.query_assets(&deps.querier, &pool_assets)?;
87                // TODO: fix this for pools with multiple assets
88                assert_eq!(assets.len(), 2);
89                // TODO: fix this for Osmosis pools
90                pool_address.expect_contract()?;
91                Ok(PriceSource::Pool {
92                    address: pool_address,
93                    pair: assets,
94                })
95            }
96            UncheckedPriceSource::LiquidityToken {} => {
97                let lp_token = AnsEntryConvertor::new(entry.clone()).lp_token()?;
98                let pairing = AnsEntryConvertor::new(lp_token.clone()).dex_asset_pairing()?;
99
100                let pool_assets = ans_host.query_assets(&deps.querier, &lp_token.assets)?;
101                // TODO: fix this for multiple pools with same pair
102                // TODO: don't use unwrap
103                let pool_address = ans_host
104                    .query_asset_pairing(&deps.querier, &pairing)?
105                    .pop()
106                    .unwrap()
107                    .pool_address;
108                Ok(PriceSource::LiquidityToken {
109                    pool_assets,
110                    pool_address,
111                })
112            }
113            UncheckedPriceSource::ValueAs { asset, multiplier } => {
114                let asset_info = ans_host.query_asset(&deps.querier, &asset)?;
115                Ok(PriceSource::ValueAs {
116                    asset: asset_info,
117                    multiplier,
118                })
119            }
120            UncheckedPriceSource::None => Ok(PriceSource::None),
121        }
122    }
123}
124
125/// Provides information on how to calculate the value of an asset
126#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, JsonSchema)]
127#[non_exhaustive]
128pub enum PriceSource {
129    /// Should only be used for the base asset
130    None,
131    /// A pool name of an asset/asset pair
132    /// Both assets must be defined in the Vault_assets state
133    Pool {
134        address: PoolAddress,
135        /// two assets that make up a pair in the pool
136        pair: Vec<AssetInfo>,
137    },
138    /// Liquidity pool token
139    LiquidityToken {
140        pool_assets: Vec<AssetInfo>,
141        pool_address: PoolAddress,
142    },
143    /// Asset will be valued as if they are ValueAs.asset tokens
144    ValueAs {
145        asset: AssetInfo,
146        multiplier: Decimal,
147    },
148}
149
150impl PriceSource {
151    /// Returns the assets that are required to calculate the price of the asset
152    /// Panics if the price source is None
153    pub fn dependencies(&self, asset: &AssetInfo) -> Vec<AssetInfo> {
154        match self {
155            // return the other asset as the dependency
156            PriceSource::Pool { pair, .. } => {
157                pair.iter().filter(|a| *a != asset).cloned().collect()
158            }
159            PriceSource::LiquidityToken { pool_assets, .. } => pool_assets.clone(),
160            PriceSource::ValueAs { asset, .. } => vec![asset.clone()],
161            PriceSource::None => vec![],
162        }
163    }
164
165    /// Calculates the conversion ratio of the asset.
166    pub fn conversion_rates(
167        &self,
168        deps: Deps,
169        asset: &AssetInfo,
170    ) -> AbstractResult<Vec<AssetConversion>> {
171        // Is there a reference to calculate the price?
172        // each method must return the price of the asset in terms of the another asset, accept for the base asset.
173        match self {
174            // A Pool refers to a swap pair, the ratio of assets in the pool represents the price of the asset in the other asset's denom
175            PriceSource::Pool { address, pair } => self
176                .trade_pair_price(deps, asset, &address.expect_contract()?, pair)
177                .map(|e| vec![e]),
178            // Liquidity is an LP token,
179            PriceSource::LiquidityToken {
180                pool_address,
181                pool_assets,
182            } => self.lp_conversion(deps, asset, &pool_address.expect_contract()?, pool_assets),
183            // A proxy asset is used instead
184            PriceSource::ValueAs { asset, multiplier } => {
185                Ok(vec![AssetConversion::new(asset.clone(), *multiplier)])
186            }
187            // None means it's the base asset
188            PriceSource::None => Ok(vec![]),
189        }
190    }
191
192    /// Calculates the price of an asset compared to some other asset through the provided trading pair.
193    fn trade_pair_price(
194        &self,
195        deps: Deps,
196        priced_asset: &AssetInfo,
197        address: &Addr,
198        pair: &[AssetInfo],
199    ) -> AbstractResult<AssetConversion> {
200        let other_asset_info = pair.iter().find(|a| a != &priced_asset).unwrap();
201        // query assets held in pool, gives price
202        let pool_info = (
203            other_asset_info.query_balance(&deps.querier, address)?,
204            priced_asset.query_balance(&deps.querier, address)?,
205        );
206        // other / this
207        let ratio = Decimal::from_ratio(pool_info.0.u128(), pool_info.1.u128());
208        // Get the conversion ratio in the denom of this asset
209        // #other = #this * (pool_other/pool_this)
210        Ok(AssetConversion::new(other_asset_info.clone(), ratio))
211    }
212
213    /// Calculate the conversions of an LP token
214    /// Uses the lp token name to query pair pool for both assets
215    /// Returns the conversion ratio of the LP token in terms of the other asset
216    fn lp_conversion(
217        &self,
218        deps: Deps,
219        lp_asset: &AssetInfo,
220        pool_addr: &Addr,
221        pool_assets: &[AssetInfo],
222    ) -> AbstractResult<Vec<AssetConversion>> {
223        let supply: Uint128;
224        if let AssetInfo::Cw20(addr) = lp_asset {
225            supply = query_cw20_supply(&deps.querier, addr)?;
226        } else {
227            return Err(StdError::generic_err("Can't have a native LP token").into());
228        }
229        pool_assets
230            .iter()
231            .map(|asset| {
232                let pool_balance = asset
233                    .query_balance(&deps.querier, pool_addr.clone())
234                    .map_err(AbstractError::from)?;
235                Ok(AssetConversion::new(
236                    asset.clone(),
237                    Decimal::from_ratio(pool_balance.u128(), supply.u128()),
238                ))
239            })
240            .collect::<AbstractResult<Vec<AssetConversion>>>()
241    }
242}
243
244fn query_cw20_supply(querier: &QuerierWrapper, contract_addr: &Addr) -> AbstractResult<Uint128> {
245    let response: cw20::TokenInfoResponse =
246        querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
247            contract_addr: contract_addr.into(),
248            msg: to_json_binary(&cw20::Cw20QueryMsg::TokenInfo {})?,
249        }))?;
250    Ok(response.total_supply)
251}
252
253#[cfg(test)]
254mod tests {
255    use abstract_testing::prelude::*;
256    use cosmwasm_std::testing::mock_dependencies;
257    use speculoos::prelude::*;
258
259    use super::*;
260
261    // TODO: abstract_testing has a circular dependency with this package, and so the mockAns host is unable to be used.
262    mod check {
263        use cosmwasm_std::testing::mock_dependencies;
264
265        use super::*;
266        use crate::{
267            ans_host,
268            objects::{ans_host::AnsHostError, pool_id::PoolAddressBase},
269        };
270
271        #[test]
272        fn liquidity_token() -> AbstractResult<()> {
273            let mut deps = mock_dependencies();
274            // we cannot use the MockAnsHost because it has a circular dependency with this package
275            deps.querier = MockQuerierBuilder::default()
276                .with_contract_map_entries(
277                    TEST_ANS_HOST,
278                    ans_host::state::ASSET_ADDRESSES,
279                    vec![
280                        (
281                            &AssetEntry::from(TEST_ASSET_1),
282                            AssetInfo::native(TEST_ASSET_1),
283                        ),
284                        (
285                            &AssetEntry::from(TEST_ASSET_2),
286                            AssetInfo::native(TEST_ASSET_2),
287                        ),
288                    ],
289                )
290                .with_contract_map_entries(
291                    TEST_ANS_HOST,
292                    ans_host::state::ASSET_PAIRINGS,
293                    vec![(
294                        &DexAssetPairing::new(TEST_ASSET_1.into(), TEST_ASSET_2.into(), TEST_DEX),
295                        vec![PoolReference::new(
296                            TEST_UNIQUE_ID.into(),
297                            PoolAddressBase::Contract(Addr::unchecked(TEST_POOL_ADDR)),
298                        )],
299                    )],
300                )
301                .build();
302
303            let price_source = UncheckedPriceSource::LiquidityToken {};
304
305            let actual_source_res = price_source.check(
306                deps.as_ref(),
307                &AnsHost::new(Addr::unchecked(TEST_ANS_HOST)),
308                &AssetEntry::new(TEST_LP_TOKEN_NAME),
309            );
310
311            assert_that!(actual_source_res)
312                .is_ok()
313                .is_equal_to(PriceSource::LiquidityToken {
314                    pool_address: PoolAddress::contract(Addr::unchecked(TEST_POOL_ADDR)),
315                    pool_assets: vec![
316                        AssetInfo::native(TEST_ASSET_1),
317                        AssetInfo::native(TEST_ASSET_2),
318                    ],
319                });
320            Ok(())
321        }
322
323        #[test]
324        fn liquidity_token_missing_asset() -> AbstractResult<()> {
325            let mut deps = mock_dependencies();
326            deps.querier = MockQuerierBuilder::default()
327                .with_contract_map_key(
328                    TEST_ANS_HOST,
329                    ans_host::state::ASSET_ADDRESSES,
330                    &TEST_ASSET_1.into(),
331                )
332                .with_contract_map_key(
333                    TEST_ANS_HOST,
334                    ans_host::state::ASSET_ADDRESSES,
335                    &TEST_ASSET_2.into(),
336                )
337                .with_contract_map_entries(TEST_ANS_HOST, ans_host::state::ASSET_PAIRINGS, vec![])
338                .build();
339
340            let price_source = UncheckedPriceSource::LiquidityToken {};
341
342            let actual_source_res = price_source.check(
343                deps.as_ref(),
344                &AnsHost::new(Addr::unchecked(TEST_ANS_HOST)),
345                &AssetEntry::new(TEST_LP_TOKEN_NAME),
346            );
347
348            assert_that!(actual_source_res)
349                .is_err()
350                .is_equal_to(AbstractError::AnsHostError(AnsHostError::AssetNotFound {
351                    asset: AssetEntry::new(TEST_ASSET_1),
352                    ans_host: Addr::unchecked(TEST_ANS_HOST),
353                }));
354            Ok(())
355        }
356    }
357
358    mod lp_conversion {
359        use super::*;
360
361        #[test]
362        fn fail_with_native_token() -> AbstractResult<()> {
363            let deps = mock_dependencies();
364            let price_source = PriceSource::LiquidityToken {
365                pool_address: PoolAddress::contract(Addr::unchecked(TEST_POOL_ADDR)),
366                pool_assets: vec![AssetInfo::native(TEST_ASSET_1)],
367            };
368            let actual_res = price_source.lp_conversion(
369                deps.as_ref(),
370                &AssetInfo::native("aoeu"),
371                &Addr::unchecked(TEST_POOL_ADDR),
372                &[],
373            );
374            assert_that!(actual_res)
375                .is_err()
376                .is_equal_to(AbstractError::Std(StdError::generic_err(
377                    "Can't have a native LP token",
378                )));
379            Ok(())
380        }
381
382        #[test]
383        fn gets_cw20_supply() -> AbstractResult<()> {
384            let mut deps = mock_dependencies();
385            deps.querier = MockQuerierBuilder::default()
386                .with_contract_item(
387                    TEST_LP_TOKEN_ADDR,
388                    cw20_base::state::TOKEN_INFO,
389                    &cw20_base::state::TokenInfo {
390                        name: "test".to_string(),
391                        symbol: "test".to_string(),
392                        decimals: 0,
393                        total_supply: Uint128::from(100u128),
394                        mint: None,
395                    },
396                )
397                .with_smart_handler(TEST_LP_TOKEN_ADDR, |msg| {
398                    let res = match from_json::<cw20::Cw20QueryMsg>(msg).unwrap() {
399                        cw20::Cw20QueryMsg::TokenInfo {} => cw20::TokenInfoResponse {
400                            name: "test".to_string(),
401                            symbol: "test".to_string(),
402                            decimals: 0,
403                            total_supply: Uint128::from(100u128),
404                        },
405                        _ => panic!("unexpected message"),
406                    };
407
408                    Ok(to_json_binary(&res).unwrap())
409                })
410                .build();
411
412            let target_asset = AssetInfo::native(TEST_ASSET_1);
413            let price_source = PriceSource::LiquidityToken {
414                pool_address: PoolAddress::contract(Addr::unchecked(TEST_POOL_ADDR)),
415                pool_assets: vec![target_asset.clone()],
416            };
417            let actual_res = price_source.lp_conversion(
418                deps.as_ref(),
419                &AssetInfo::cw20(Addr::unchecked(TEST_LP_TOKEN_ADDR)),
420                &Addr::unchecked(TEST_POOL_ADDR),
421                &[target_asset.clone()],
422            )?;
423
424            assert_that!(actual_res).has_length(1);
425            assert_that!(actual_res[0]).is_equal_to(AssetConversion {
426                into: target_asset,
427                ratio: Decimal::zero(),
428            });
429
430            Ok(())
431        }
432    }
433}