Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cosmos lending #10

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion cosmos/contracts/finance/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "llama-finance"
version = "0.10.0"
authors = ["Łukasz Ptak <lukasz@ulam.io>"]
edition = "2018"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

Expand All @@ -20,6 +20,8 @@ schemars = "0.8.8"
serde = { version = "1.0.137", default-features = false, features = ["derive"] }
thiserror = { version = "1.0.31" }
oracle = { path = "../oracle" }
coreum-wasm-sdk = "0.2.4"
bnum = { version = "0.10.0" }

[dev-dependencies]
cosmwasm-schema = "1.0.0"
169 changes: 124 additions & 45 deletions cosmos/contracts/finance/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
use std::convert::TryInto;
use crate::external::query_price;
use crate::liquidation::{calculate_coins_value, max_liquidation_value};
use crate::math::Fixed64x64;

use bnum::cast::As;
use cosmwasm_std::{
entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Uint128, BankMsg, Coin, Addr,
entry_point, Addr, BankMsg, Binary, Coin, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Timestamp, Uint128
};
use oracle::msg::PriceResponse;

use crate::error::{ContractError, ContractResult};
use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};

use crate::state::{USER_ASSET_INFO, ASSETS, ASSET_INFO, ADMIN, UserAssetInfo, AssetConfig, AssetInfo, GLOBAL_DATA, GlobalData, RATE_DENOMINATOR, NANOSECONDS_IN_YEAR};
use crate::state::{USER_ASSET_INFO, ASSETS, ASSET_INFO, ADMIN, UserAssetInfo, AssetConfig, AssetInfo, GLOBAL_DATA, GlobalData, RATE_DENOMINATOR, SECONDS_IN_YEAR};
use crate::query::query_handler;

#[entry_point]
Expand All @@ -26,6 +27,7 @@ pub fn instantiate(
let global_data = GlobalData {
oracle: msg.oracle,
liquidation_threshold,
max_apy_bps: msg.max_apy_bps
};
GLOBAL_DATA.save(deps.storage, &global_data)?;
Ok(Response::default())
Expand Down Expand Up @@ -63,8 +65,25 @@ pub fn execute(
ExecuteMsg::UpdateUserAssetInfo { user_addr } => {
update_user_asset_info(deps, env, user_addr)
},
ExecuteMsg::UpdateAsset { denom, decimals, target_utilization_rate_bps, min_rate, optimal_rate, max_rate } => {
update_asset(deps, env, info, denom, target_utilization_rate_bps, decimals, min_rate, optimal_rate, max_rate)
ExecuteMsg::UpdateAsset {
denom,
decimals,
target_utilization_rate_bps,
min_rate_secondly,
optimal_rate_secondly,
max_rate_secondly
} => {
update_asset(
deps,
env,
info,
denom,
target_utilization_rate_bps,
decimals,
min_rate_secondly.into(),
optimal_rate_secondly.into(),
max_rate_secondly.into()
)
}
}
}
Expand Down Expand Up @@ -128,24 +147,50 @@ fn convert_l_asset_to_asset(
amount.checked_multiply_ratio(asset_info.total_deposit, asset_info.total_l_asset).ok().ok_or(ContractError::TooManyLAssets{})
}

fn calculate_rate(asset_info: &AssetInfo) -> ContractResult<u32> {
/// Calculate secondly rate based on asset utilization
fn calculate_rate(asset_info: &AssetInfo) -> ContractResult<Fixed64x64> {
let config = &asset_info.asset_config;
let util_rate = asset_info.total_borrow.checked_multiply_ratio(RATE_DENOMINATOR, asset_info.total_deposit).ok().ok_or(ContractError::InvalidUtilizationRatio { })?;

if let Ok(util_delta) = util_rate.checked_sub(Uint128::from(config.target_utilization_rate_bps)) {
let max_util_delta = RATE_DENOMINATOR.checked_sub(config.target_utilization_rate_bps).ok_or(ContractError::InvalidTargetUtilization { })?;
let rate_delta = config.max_rate.checked_sub(config.optimal_rate).ok_or(ContractError::InvalidMaxRate { })?;
let util_delta_rate = util_delta.checked_multiply_ratio(rate_delta, RATE_DENOMINATOR).ok().ok_or(ContractError::InvalidUtilizationRatio { })?;
let nonlinear_rate = util_delta_rate.checked_multiply_ratio(RATE_DENOMINATOR, max_util_delta).ok().ok_or(ContractError::InvalidTargetUtilization { })?;
nonlinear_rate.u128().try_into().ok().ok_or(ContractError::InvalidTargetUtilization { })
let utilization = Fixed64x64::from_num_denom_128(asset_info.total_borrow, asset_info.total_deposit)?;
let target_utilization = Fixed64x64::from_num_denom(
config.target_utilization_bps.into(),
10_000
);

let res: Option<Fixed64x64> = if utilization > target_utilization {
// function of rate per second from utilization is:
// rate = (max_r - optimal_r) * (util - target_util) / (1 - target_util) + optimal_r
(|| {
let rate_delta = config.max_rate_secondly.checked_sub(config.optimal_rate_secondly)?;
let util_delta = utilization.checked_sub(target_utilization)?;
let max_util_delta = Fixed64x64::ONE.checked_sub(target_utilization)?;
let added_rate = rate_delta.checked_multiply_ratio(util_delta, max_util_delta)?;

config.optimal_rate_secondly.checked_add(added_rate)
})()
} else {
let rate_delta = config.optimal_rate.checked_sub(config.min_rate).ok_or(ContractError::InvalidOptimalRate { })?;
let linear_rate = util_rate.checked_multiply_ratio(rate_delta, config.target_utilization_rate_bps).ok().ok_or(ContractError::InvalidTargetUtilization{})?;
let rate = linear_rate.checked_add(Uint128::from(config.min_rate)).ok().ok_or(ContractError::InvalidMinRate { })?;
rate.u128().try_into().ok().ok_or(ContractError::InvalidMinRate { })
// function of rate per second from utilization is:
// rate = (optimal_r - min_r) * util / target_util + min_r
(|| {
let rate_delta = config.optimal_rate_secondly.checked_sub(config.min_rate_secondly)?;
let added_rate = utilization.checked_multiply_ratio(rate_delta, target_utilization)?;
config.min_rate_secondly.checked_add(added_rate)
})()
};

res.ok_or(ContractError::InvalidRate { })
}


fn time_delta_seconds(time_prev: Timestamp, time_curr: Timestamp) -> ContractResult<u64> {
if time_curr < time_prev {
Err(ContractError::ClockSkew { })
} else {
Ok(time_curr.minus_nanos(time_prev.nanos()).seconds())
}
}


// update each asset's cumulative interest and user's borrow amount
fn update(
deps: &mut DepsMut,
env: &Env,
Expand All @@ -155,31 +200,48 @@ fn update(
let assets = ASSETS.load(deps.storage)?;
for denom in assets.iter() {
let mut asset_info = ASSET_INFO.load(deps.storage, denom)?;
let rate = if asset_info.total_deposit.is_zero() {
asset_info.asset_config.min_rate

// borrow APR calculation
let rate_per_sec = if asset_info.total_deposit.is_zero() {
asset_info.asset_config.min_rate_secondly
} else {
calculate_rate(&asset_info)?
};
let time_elapsed = now.nanos().checked_sub(asset_info.last_update.nanos()).ok_or(ContractError::ClockSkew { })?;
let new_interest_after_year = asset_info.cumulative_interest.checked_multiply_ratio(rate, RATE_DENOMINATOR).ok().ok_or(ContractError::InvalidRate { })?;
let new_interest = new_interest_after_year.checked_multiply_ratio(time_elapsed, NANOSECONDS_IN_YEAR).ok().ok_or(ContractError::InvalidTimeElapsed{})?;
let final_cumulative_rate = asset_info.cumulative_interest.saturating_add(new_interest);

let time_elapsed = time_delta_seconds(asset_info.last_update, now)?;

// yield = ((1 + rate_per_second) ** time_elapsed)
let yield_adj = Fixed64x64::ONE
.checked_add(rate_per_sec).unwrap() // Shouldn't ever overflow
.checked_pow(time_elapsed as u32).ok_or(ContractError::InvalidRate { })?; // Cast will only overflow after >60yrs of inactivity

let final_cumulative_interest = asset_info.cumulative_interest
.checked_mul(yield_adj)
.ok_or(ContractError::InvalidRate { })?;

let user_key = (user, denom.as_ref());
if let Ok(mut user_asset_info) = USER_ASSET_INFO.load(deps.storage, user_key) {
let final_borrow_amount = user_asset_info.borrow_amount.checked_multiply_ratio(final_cumulative_rate, asset_info.cumulative_interest).ok().ok_or(ContractError::InvalidCumulativeInterest{})?;
let final_borrow_amount = user_asset_info.borrow_amount.checked_multiply_ratio(
final_cumulative_interest.raw().as_::<u128>(),
asset_info.cumulative_interest.raw().as_::<u128>()
).map_err(|_| ContractError::InvalidCumulativeInterest{})?;
user_asset_info.borrow_amount = final_borrow_amount;
USER_ASSET_INFO.save(deps.storage, user_key, &user_asset_info)?;
}
let final_total_borrow = asset_info.total_borrow.checked_multiply_ratio(final_cumulative_rate, asset_info.cumulative_interest).ok().ok_or(ContractError::InvalidCumulativeInterest{})?;
let new_deposit = final_total_borrow.saturating_sub(asset_info.total_borrow);
let final_total_borrow = asset_info.total_borrow.checked_multiply_ratio(
final_cumulative_interest.raw().as_::<u128>(),
asset_info.cumulative_interest.raw().as_::<u128>()
).map_err(|_| ContractError::InvalidCumulativeInterest{})?;

let new_deposit: Uint128 = final_total_borrow.saturating_sub(asset_info.total_borrow);
let final_total_deposit = asset_info.total_deposit.saturating_add(new_deposit);

asset_info.cumulative_interest = final_cumulative_rate;

asset_info.cumulative_interest = final_cumulative_interest;
asset_info.total_borrow = final_total_borrow;
asset_info.total_deposit = final_total_deposit;
asset_info.last_update = now;
asset_info.apr = Uint128::from(rate);
asset_info.rate_per_sec = rate_per_sec;
ASSET_INFO.save(deps.storage, denom, &asset_info)?;
}
Ok(())
Expand All @@ -195,20 +257,20 @@ fn update_user_asset_info(
Ok(Response::default())
}

// Entry point for depositing native tokens into contract
// Deposited tokens immediatelly start accruing interest
fn deposit(
mut deps: DepsMut,
env: Env,
info: MessageInfo,
) -> ContractResult<Response> {
update(&mut deps, &env, &info.sender)?;


for coin in info.funds.iter() {
// Fetch global cumulative interest for this asset
let mut asset_info = ASSET_INFO.load(deps.storage, &coin.denom)?;
let l_asset_amt = convert_asset_to_l_asset(coin.amount, &asset_info)?;


USER_ASSET_INFO.update(
deps.storage,
(&info.sender, &coin.denom),
Expand Down Expand Up @@ -466,8 +528,8 @@ fn update_prices(
for denom in assets.iter() {
let PriceResponse {
price,
symbol: _,
precision,
actual_timespan_seconds: _
} = query_price(deps.as_ref(), global_data.oracle.clone(), denom.clone())?;
let mut asset_info = ASSET_INFO.load(deps.storage, denom)?;
asset_info.price = price;
Expand Down Expand Up @@ -547,25 +609,42 @@ fn update_asset(
denom: String,
target_utilization_rate_bps: u32,
decimals: u16,
min_rate: u32,
optimal_rate: u32,
max_rate: u32,
min_rate_secondly: Fixed64x64,
optimal_rate_secondly: Fixed64x64,
max_rate_secondly: Fixed64x64,
) -> ContractResult<Response> {
if ADMIN.load(deps.storage)? != info.sender {
return Err(ContractError::Unauthorized { });
}

if min_rate_secondly > optimal_rate_secondly || optimal_rate_secondly > max_rate_secondly {
return Err(ContractError::InvalidNonIncreasingRate { });
}

if target_utilization_rate_bps > 10_000 {
return Err(ContractError::InvalidTargetUtilization { });
}

let glb = GLOBAL_DATA.load(deps.storage)?;

let max_rate_annual = max_rate_secondly
.checked_pow(SECONDS_IN_YEAR)
.ok_or(ContractError::MaxAPYExceeded { })?;
if max_rate_annual > Fixed64x64::from_num_denom(glb.max_apy_bps as u64, RATE_DENOMINATOR as u64) {
return Err(ContractError::MaxAPYExceeded { });
}

let mut new_asset = false;
ASSET_INFO.update(deps.storage, &denom,
|asset_info| -> ContractResult<AssetInfo> {
match asset_info {
Some(mut asset_info) => {
asset_info.asset_config = AssetConfig {
target_utilization_rate_bps,
target_utilization_bps: target_utilization_rate_bps,
decimals,
min_rate,
optimal_rate,
max_rate,
min_rate_secondly,
optimal_rate_secondly,
max_rate_secondly,
};
Ok(asset_info)
},
Expand All @@ -574,19 +653,19 @@ fn update_asset(
Ok(
AssetInfo {
denom: denom.clone(),
apr: Uint128::zero(),
rate_per_sec: min_rate_secondly,
total_deposit: Uint128::zero(),
total_borrow: Uint128::zero(),
total_l_asset: Uint128::zero(),
total_collateral: Uint128::zero(),
cumulative_interest: Uint128::from(1u128<<64),
cumulative_interest: Fixed64x64::ONE,
last_update: env.block.time,
asset_config: AssetConfig {
target_utilization_rate_bps,
target_utilization_bps: target_utilization_rate_bps,
decimals,
min_rate,
optimal_rate,
max_rate,
min_rate_secondly,
optimal_rate_secondly,
max_rate_secondly,
},
price: Uint128::zero(),
price_precision: Uint128::zero(),
Expand Down
11 changes: 10 additions & 1 deletion cosmos/contracts/finance/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ pub enum ContractError {
#[error("clock skew")]
ClockSkew {},

#[error("invalid rate")]
#[error("max APY exceeded")]
MaxAPYExceeded {},

#[error("unexpected error while calculating rate")]
InvalidRate {},

#[error("invalid cumulative interest")]
Expand All @@ -63,6 +66,9 @@ pub enum ContractError {
#[error("invalid min rate")]
InvalidMinRate {},

#[error("invalid non-increasing rate")]
InvalidNonIncreasingRate {},

#[error("invalid liquidation threshold")]
InvalidLiquidationThreshold {},

Expand Down Expand Up @@ -93,6 +99,9 @@ pub enum ContractError {

#[error("unsafe ltv")]
UnsafeLTV {},

#[error("ratio calculation overflow")]
RatioCalculationOverflow {},
}

pub type ContractResult<T> = Result<T, ContractError>;
Loading