Skip to content
This repository was archived by the owner on Oct 16, 2023. It is now read-only.

Health check#7

Merged
grod220 merged 4 commits intomasterfrom
oracle-health
Aug 11, 2022
Merged

Health check#7
grod220 merged 4 commits intomasterfrom
oracle-health

Conversation

@grod220
Copy link
Contributor

@grod220 grod220 commented Jul 21, 2022

The main feature is the health check assertion callback ensuring only healthy state changes are allowed. However, lots of little things are included here:

  • Oracle integration (using mock)
  • LTV and liquidation threshold now queried from red bank

}

pub fn assert_health(deps: DepsMut, env: Env, token_id: NftTokenId) -> ContractResult<Response> {
let position = query_position(deps.as_ref(), &env, token_id)?;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm directly calling query_position here. Not sure if that is more normal or rather doing a query hitting that position msg endpoint.

@grod220 grod220 force-pushed the oracle-health branch 2 times, most recently from c07984e to 0fc8452 Compare July 25, 2022 08:12
&QueryMsg::Market {
asset: asset_info.into(),
},
)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some optimization could be to use QueryMsg::MarketsList to query markets in one call.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, will investigate

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my case, MarketsList won't help me because it returns Vec<MarketInfo> which doesn't have max_tvl and liquidation_threshold fields. Will need to still call QueryMsg::Market individually.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can fix that

I will work on red bank after finishing rewards collector. I will see what i can do

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could query by list of denoms but for now I don't know if it is worth

let margin = HEALTH_FACTOR_MARGIN.load(deps.storage)?;
let hf_str = position
.health_factor
.map_or("null".to_string(), |dec| dec.to_string());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest I am not sure if this null is good. Maybe if it is null then we don't add it to attributes. Maybe we could add something more descriptive.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would n/a be better? I wonder if it would be more difficult for the consumers of these attributes if the field sometimes wasn't present 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

N/a makes sense to me

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

n/a - perfect!

}
}

// TODO: After mars-core updates oracle contract to CosmWasm 1.0 should update this mock to return MarsDecimal
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mars-core oracle already updated

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would need a version bump here: https://crates.io/crates/mars-core to import 🙏

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please use local deps for now ('lib = { path = "../lib" }')? Maybe it is better to update crates.io if we have everything ready (after testing and audit).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type lives in mars-core so I don't have access to the file to add as a local dependency here. Maybe I'm missing something? Or are you suggesting copying over Mars' Decimal struct file to Rover (temporarily)?

Copy link
Contributor

@piobab piobab Aug 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh! In that case, this code comment is invalid because there is not special Mars Decimal struct anyway. Happy to see it gone! Will remove this.

}

// Schema reference: https://github.com/mars-protocol/mars-core/blob/master/packages/mars-core/src/red_bank/mod.rs#L47
// TODO: After mars-core updates oracle contract to CosmWasm 1.0 should update this mock to return MarsDecimal
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

Copy link
Contributor Author

@grod220 grod220 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lots of changes from borrow PR relating to cw20s. Going to do a bit rebase with this.

let margin = HEALTH_FACTOR_MARGIN.load(deps.storage)?;
let hf_str = position
.health_factor
.map_or("null".to_string(), |dec| dec.to_string());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would n/a be better? I wonder if it would be more difficult for the consumers of these attributes if the field sometimes wasn't present 🤔

}
}

// TODO: After mars-core updates oracle contract to CosmWasm 1.0 should update this mock to return MarsDecimal
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would need a version bump here: https://crates.io/crates/mars-core to import 🙏

&QueryMsg::Market {
asset: asset_info.into(),
},
)?;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, will investigate

Base automatically changed from borrow to master August 4, 2022 18:12
};

let liquidatable = lqdt_health_factor.map_or_else(|| false, |hf| hf <= Decimal::one());
let above_max_ltv = max_ltv_health_factor.map_or_else(|| false, |hf| hf <= Decimal::one());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is different than fields. See: https://marslend.slack.com/archives/C03QSLT0LPJ/p1659677771072499. Having debt shares that are zero value is not a liquidatable event.

@piobab piobab self-requested a review August 10, 2022 20:40
ORACLE.save(deps.storage, &unchecked.check(deps.api)?)?;
response = response
.add_attribute("key", "oracle")
.add_attribute("value", unchecked.0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a fan of unchecked.0, because people will need to guess what "0" standards for. we know it stands for the contract's address, but it can well be something else.

instead, OracleBase can implement an address method that return the address:

struct OracleBase<T>(T); // T should be private

impl<T> OracleBase<T> {
    pub fn address(&self) -> &T {
        return &self.0;
    }
}

response = response.add_attribute("value", unchecked.address());

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, this is much better. Actually think a getter would be even better, but those don't exist in rust. This will also require a new method since you can't use the constructor directly now.

Comment on lines +58 to +59
let liquidatable = lqdt_health_factor.map_or_else(|| false, |hf| hf <= Decimal::one());
let above_max_ltv = max_ltv_health_factor.map_or_else(|| false, |hf| hf <= Decimal::one());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since false is a constant, you can use map_or instead of map_or_else

*_or_else methods are for non-constant values that need to be evaluated at runtime

Suggested change
let liquidatable = lqdt_health_factor.map_or_else(|| false, |hf| hf <= Decimal::one());
let above_max_ltv = max_ltv_health_factor.map_or_else(|| false, |hf| hf <= Decimal::one());
let liquidatable = lqdt_health_factor.map_or(false, |hf| hf <= Decimal::one());
let above_max_ltv = max_ltv_health_factor.map_or(false, |hf| hf <= Decimal::one());

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know that distinction

Comment on lines +71 to +76
pub fn assert_below_max_ltv(
deps: DepsMut,
env: Env,
token_id: NftTokenId,
) -> ContractResult<Response> {
let position = compute_health(deps.as_ref(), &env, token_id)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub fn assert_below_max_ltv(
deps: DepsMut,
env: Env,
token_id: NftTokenId,
) -> ContractResult<Response> {
let position = compute_health(deps.as_ref(), &env, token_id)?;
pub fn assert_below_max_ltv(
deps: Deps,
env: Env,
token_id: NftTokenId,
) -> ContractResult<Response> {
let position = compute_health(deps, &env, token_id)?;

token_id: token_id.to_string(),
assets: get_assets(deps, token_id)?,
debt_shares: get_debt_shares(deps, token_id)?,
coin_assets: coin_asset_values,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why renaming assets to coin_assets? feels redundant. imo, it can either be coins or assets, but no coin_assets

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to distinguish the difference between:

  • Assets -> coins or vaults that are a positive contributor to your health factor
  • Debts -> coins you owe

But maybe renaming the state from ASSETS to COIN_BALANCES and this field to coins communicates this enough.

denom: denom.clone(),
amount: *amount,
price,
total_value,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo this field should be called value instead of total_value. "total" means the sum of all coins' values. this is the value of just one coin.

Comment on lines +47 to +48
red_bank: RedBankBase("redbankaddr".to_string()),
oracle: OracleBase("oracle_contract".to_string()),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you make the format of the mock contract addresses consistent

Comment on lines +78 to +161
CoinInfo {
denom: "coin_1".to_string(),
max_ltv: Decimal::from_atomics(7u128, 1).unwrap(),
liquidation_threshold: Decimal::from_atomics(78u128, 2).unwrap(),
price: Decimal::from_atomics(10u128, 0).unwrap(),
},
CoinInfo {
denom: "coin_2".to_string(),
max_ltv: Decimal::from_atomics(7u128, 1).unwrap(),
liquidation_threshold: Decimal::from_atomics(78u128, 2).unwrap(),
price: Decimal::from_atomics(10u128, 0).unwrap(),
},
CoinInfo {
denom: "coin_3".to_string(),
max_ltv: Decimal::from_atomics(7u128, 1).unwrap(),
liquidation_threshold: Decimal::from_atomics(78u128, 2).unwrap(),
price: Decimal::from_atomics(10u128, 0).unwrap(),
},
CoinInfo {
denom: "coin_4".to_string(),
max_ltv: Decimal::from_atomics(7u128, 1).unwrap(),
liquidation_threshold: Decimal::from_atomics(78u128, 2).unwrap(),
price: Decimal::from_atomics(10u128, 0).unwrap(),
},
CoinInfo {
denom: "coin_5".to_string(),
max_ltv: Decimal::from_atomics(7u128, 1).unwrap(),
liquidation_threshold: Decimal::from_atomics(78u128, 2).unwrap(),
price: Decimal::from_atomics(10u128, 0).unwrap(),
},
CoinInfo {
denom: "coin_6".to_string(),
max_ltv: Decimal::from_atomics(7u128, 1).unwrap(),
liquidation_threshold: Decimal::from_atomics(78u128, 2).unwrap(),
price: Decimal::from_atomics(10u128, 0).unwrap(),
},
CoinInfo {
denom: "coin_7".to_string(),
max_ltv: Decimal::from_atomics(7u128, 1).unwrap(),
liquidation_threshold: Decimal::from_atomics(78u128, 2).unwrap(),
price: Decimal::from_atomics(10u128, 0).unwrap(),
},
CoinInfo {
denom: "coin_8".to_string(),
max_ltv: Decimal::from_atomics(7u128, 1).unwrap(),
liquidation_threshold: Decimal::from_atomics(78u128, 2).unwrap(),
price: Decimal::from_atomics(10u128, 0).unwrap(),
},
CoinInfo {
denom: "coin_9".to_string(),
max_ltv: Decimal::from_atomics(7u128, 1).unwrap(),
liquidation_threshold: Decimal::from_atomics(78u128, 2).unwrap(),
price: Decimal::from_atomics(10u128, 0).unwrap(),
},
CoinInfo {
denom: "coin_10".to_string(),
max_ltv: Decimal::from_atomics(7u128, 1).unwrap(),
liquidation_threshold: Decimal::from_atomics(78u128, 2).unwrap(),
price: Decimal::from_atomics(10u128, 0).unwrap(),
},
CoinInfo {
denom: "coin_11".to_string(),
max_ltv: Decimal::from_atomics(7u128, 1).unwrap(),
liquidation_threshold: Decimal::from_atomics(78u128, 2).unwrap(),
price: Decimal::from_atomics(10u128, 0).unwrap(),
},
CoinInfo {
denom: "coin_12".to_string(),
max_ltv: Decimal::from_atomics(7u128, 1).unwrap(),
liquidation_threshold: Decimal::from_atomics(78u128, 2).unwrap(),
price: Decimal::from_atomics(10u128, 0).unwrap(),
},
CoinInfo {
denom: "coin_13".to_string(),
max_ltv: Decimal::from_atomics(7u128, 1).unwrap(),
liquidation_threshold: Decimal::from_atomics(78u128, 2).unwrap(),
price: Decimal::from_atomics(10u128, 0).unwrap(),
},
CoinInfo {
denom: "coin_14".to_string(),
max_ltv: Decimal::from_atomics(7u128, 1).unwrap(),
liquidation_threshold: Decimal::from_atomics(78u128, 2).unwrap(),
price: Decimal::from_atomics(10u128, 0).unwrap(),
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may be a good idea to use a helper function to build this vector instead of typing it out, e.g.

let build_mock_coin_infos = |count: usize| -> Vec<CoinInfo> {
    (1..count)
        .into_iter()
        .map(|i| CoinInfo {
            denom: format!("coin_{}", i),
            max_ltv: Decimal::from_atomics(7u128, 1).unwrap(),
            liquidation_threshold: Decimal::from_atomics(78u128, 2).unwrap(),
            price: Decimal::from_atomics(10u128, 0).unwrap(),
        })
        .collect()
};

let coin_infos = build_mock_coin_infos(50);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very nice

Comment on lines +29 to +35
let response: Decimal = querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
contract_addr: self.0.to_string(),
msg: to_binary(&QueryMsg::AssetPrice {
denom: denom.to_string(),
})?,
}))?;
Ok(response)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a more succinct implementation. maybe works, haven't tried

Suggested change
let response: Decimal = querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
contract_addr: self.0.to_string(),
msg: to_binary(&QueryMsg::AssetPrice {
denom: denom.to_string(),
})?,
}))?;
Ok(response)
querier.query_wasm_smart(
self.0.to_string(),
&QueryMsg::AssetPrice {
denom: denom.to_string(),
},
)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed it does

pub assets_value: Decimal,
/// Total value of debts
pub debts_value: Decimal,
/// The sum of the value of all assets (multiplied by their liquidation threshold) over the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i feel the use of the term "liquidation threshold" is very confusing here.

imo "liquidation threshold" is a single constant number. if my health factor is below it, i'm liquidatable. otherwise i'm not liquidatable.

the term to be used here should be something like "collateral factor" or "collateral power"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Liquidation thresholds will always be different for different assets though.

Max LTV -- the most we'll allow a position to be taken out
Liquidation threshold -- the LTV where you get liquidated

There will be a wide variety of values for both of these.

Comment on lines +7 to +8
/// Total value of assets
pub assets_value: Decimal,
Copy link
Contributor

@larry0x larry0x Aug 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo if it's the total value of assets, it should be named total_asset_value

if it's the value of a single asset, it should be asset_value

at least this is how i name my variables it in Fields

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense. However, would call it: total_assets_value

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uh i don’t think that’s correct english grammar

Copy link
Contributor Author

@grod220 grod220 Aug 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How so? Assets is the plural of asset. So the combined value of all assets could be: total_value_of_assets or total_assets_value.

pub denom: String,
pub shares: Uint128,
pub total_value: Decimal,
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@larry0x so should we expose the shares or not here? I'm not sure it really communicates anything to the frontend. I wonder if instead we should do the math to calculate the amount underlying this and send that back instead.

Copy link
Contributor

@larry0x larry0x Aug 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i like returning the underlying amount instead. if someone wants to know the shares they can use raw query.

another benefit of returning the underlying amount is, instead of having two structs CoinValue and DebtShareValue, we can have only one (CoinValue) and use it for both assets and debts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, think this makes sense. But I want to create a follow up PR for this (given this one is nearly three weeks old).

@grod220 grod220 requested a review from larry0x August 11, 2022 12:12
@grod220 grod220 merged commit 3e83ad2 into master Aug 11, 2022
@grod220 grod220 deleted the oracle-health branch August 11, 2022 13:49
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants