From 0c5ac21e0f75683ca0440641e79f6745e970b765 Mon Sep 17 00:00:00 2001 From: 0xxgen1 <0xxgen@solend.fi> Date: Wed, 2 Oct 2024 01:21:21 +0100 Subject: [PATCH 1/9] eMode draft impl (WIP) --- contracts/sources/cell.move | 4 + contracts/sources/lending_market.move | 17 ++ contracts/sources/obligation.move | 232 +++++++++++++++++++++----- contracts/sources/reserve.move | 16 ++ contracts/sources/reserve_config.move | 133 +++++++++++++++ 5 files changed, 362 insertions(+), 40 deletions(-) diff --git a/contracts/sources/cell.move b/contracts/sources/cell.move index ec46eda..c099b28 100644 --- a/contracts/sources/cell.move +++ b/contracts/sources/cell.move @@ -16,6 +16,10 @@ module suilend::cell { public fun get(cell: &Cell): &Element { option::borrow(&cell.element) } + + public fun get_mut(cell: &mut Cell): &mut Element { + option::borrow_mut(&mut cell.element) + } public fun destroy(cell: Cell): Element { let Cell { element } = cell; diff --git a/contracts/sources/lending_market.move b/contracts/sources/lending_market.move index 45db586..beaea94 100644 --- a/contracts/sources/lending_market.move +++ b/contracts/sources/lending_market.move @@ -819,6 +819,23 @@ module suilend::lending_market { reserve::update_reserve_config

(reserve, config); } + // TODO:Consider taking EModeConfig as param.. + public fun set_emode_for_pair( + _: &LendingMarketOwnerCap

, + lending_market: &mut LendingMarket

, + reserve_array_index: u64, + pair_reserve_array_index: u64, + open_ltv_pct: u8, + close_ltv_pct: u8, + ) { + assert!(lending_market.version == CURRENT_VERSION, EIncorrectVersion); + + let reserve = vector::borrow_mut(&mut lending_market.reserves, reserve_array_index); + assert!(reserve::coin_type(reserve) == type_name::get(), EWrongType); + + reserve::set_emode_for_pair

(reserve, pair_reserve_array_index, open_ltv_pct, close_ltv_pct); + } + public fun add_pool_reward( _: &LendingMarketOwnerCap

, lending_market: &mut LendingMarket

, diff --git a/contracts/sources/obligation.move b/contracts/sources/obligation.move index f6cff98..edeb366 100644 --- a/contracts/sources/obligation.move +++ b/contracts/sources/obligation.move @@ -1,6 +1,7 @@ module suilend::obligation { // === Imports === use std::type_name::{TypeName, Self}; + use std::option::{Self, Option, some, none, is_some}; use sui::object::{Self, UID, ID}; use sui::balance::{Balance}; use std::vector::{Self}; @@ -9,7 +10,9 @@ module suilend::obligation { use sui::tx_context::{TxContext}; use suilend::reserve::{Self, Reserve, config}; use suilend::reserve_config::{ - open_ltv, + ReserveConfig, + EmodeConfig, + open_ltv, close_ltv, borrow_weight, liquidation_bonus, @@ -167,6 +170,12 @@ module suilend::obligation { } } + struct UnweightedBorrow has copy, drop { + reserve_array_index: u64, + borrow_weighted_value_usd: Decimal, + borrow_weighted_value_upper_bound_usd: Decimal, + } + /// update the obligation's borrowed amounts and health values. this is /// called by the lending market prior to any borrow, withdraw, or liquidate operation. @@ -175,6 +184,71 @@ module suilend::obligation { reserves: &mut vector>, clock: &Clock ) { + let i = 0; + let unweighted_borrowed_value_usd = decimal::from(0); + let weighted_borrowed_value_usd = decimal::from(0); + let weighted_borrowed_value_upper_bound_usd = decimal::from(0); + let borrowing_isolated_asset = false; + + let basket: vector = vector::empty(); + + while (i < vector::length(&obligation.borrows)) { + let borrow = vector::borrow_mut(&mut obligation.borrows, i); + + let borrow_reserve = vector::borrow_mut(reserves, borrow.reserve_array_index); + reserve::compound_interest(borrow_reserve, clock); + reserve::assert_price_is_fresh(borrow_reserve, clock); + + compound_debt(borrow, borrow_reserve); + + let market_value = reserve::market_value(borrow_reserve, borrow.borrowed_amount); + let market_value_upper_bound = reserve::market_value_upper_bound( + borrow_reserve, + borrow.borrowed_amount + ); + + borrow.market_value = market_value; + unweighted_borrowed_value_usd = add(unweighted_borrowed_value_usd, market_value); + + let borrow_weighted_value_usd = mul( + market_value, + borrow_weight(config(borrow_reserve)) + ); + + weighted_borrowed_value_usd = add( + weighted_borrowed_value_usd, + borrow_weighted_value_usd + ); + + let borrow_weighted_value_upper_bound_usd = mul( + market_value_upper_bound, + borrow_weight(config(borrow_reserve)) + ); + + weighted_borrowed_value_upper_bound_usd = add( + weighted_borrowed_value_upper_bound_usd, + borrow_weighted_value_upper_bound_usd, + ); + + vector::push_back(&mut basket, UnweightedBorrow { + reserve_array_index: borrow.reserve_array_index, + borrow_weighted_value_usd, + borrow_weighted_value_upper_bound_usd, + }); + + if (isolated(config(borrow_reserve))) { + borrowing_isolated_asset = true; + }; + + i = i + 1; + }; + + obligation.unweighted_borrowed_value_usd = unweighted_borrowed_value_usd; + obligation.weighted_borrowed_value_usd = weighted_borrowed_value_usd; + obligation.weighted_borrowed_value_upper_bound_usd = weighted_borrowed_value_upper_bound_usd; + + obligation.borrowing_isolated_asset = borrowing_isolated_asset; + let i = 0; let deposited_value_usd = decimal::from(0); let allowed_borrow_value_usd = decimal::from(0); @@ -199,6 +273,14 @@ module suilend::obligation { deposit.market_value = market_value; deposited_value_usd = add(deposited_value_usd, market_value); + + let (emode_allowed, emode_unhealthy) = collect_emode_values( + market_value, + market_value_lower_bound, + &mut basket, + config(deposit_reserve), + ); + allowed_borrow_value_usd = add( allowed_borrow_value_usd, mul( @@ -220,57 +302,127 @@ module suilend::obligation { obligation.deposited_value_usd = deposited_value_usd; obligation.allowed_borrow_value_usd = allowed_borrow_value_usd; obligation.unhealthy_borrow_value_usd = unhealthy_borrow_value_usd; + } - let i = 0; - let unweighted_borrowed_value_usd = decimal::from(0); - let weighted_borrowed_value_usd = decimal::from(0); - let weighted_borrowed_value_upper_bound_usd = decimal::from(0); - let borrowing_isolated_asset = false; - - while (i < vector::length(&obligation.borrows)) { - let borrow = vector::borrow_mut(&mut obligation.borrows, i); + fun collect_emode_values( + deposit_value: Decimal, // vs. borrow_weighted_value + deposit_value_lower_bound: Decimal, // vs. borrow_weighted_value_upper_bound_usd + basket: &mut vector, + config: &ReserveConfig, + ): (Option, Option) { + if (!reserve_config::has_emode_config(config)) { + return (none(), none()); + }; - let borrow_reserve = vector::borrow_mut(reserves, borrow.reserve_array_index); - reserve::compound_interest(borrow_reserve, clock); - reserve::assert_price_is_fresh(borrow_reserve, clock); + let emode_config = reserve_config::get_emode_config(config); - compound_debt(borrow, borrow_reserve); + let len = vector::length(basket); + let emode_allowed = decimal::from(0); + let emode_unhealthy = decimal::from(0); + + let residual_deposit_value = deposit_value; + let residual_deposit_value_lower_bound = deposit_value_lower_bound; - let market_value = reserve::market_value(borrow_reserve, borrow.borrowed_amount); - let market_value_upper_bound = reserve::market_value_upper_bound( - borrow_reserve, - borrow.borrowed_amount - ); + while (len > 0) { + let borrow = vector::borrow_mut(basket, len - 1); - borrow.market_value = market_value; - unweighted_borrowed_value_usd = add(unweighted_borrowed_value_usd, market_value); - weighted_borrowed_value_usd = add( - weighted_borrowed_value_usd, - mul( - market_value, - borrow_weight(config(borrow_reserve)) - ) - ); - weighted_borrowed_value_upper_bound_usd = add( - weighted_borrowed_value_upper_bound_usd, - mul( - market_value_upper_bound, - borrow_weight(config(borrow_reserve)) - ) + let is_correlated = reserve_config::is_correlated( + emode_config, + borrow.reserve_array_index ); - if (isolated(config(borrow_reserve))) { - borrowing_isolated_asset = true; + if (!is_correlated) { + continue + } else { + // Collect values for emode_allowed + collect_emode( + &mut emode_allowed, + &mut residual_deposit_value_lower_bound, + &mut borrow.borrow_weighted_value_upper_bound_usd, + ); + + // Collect values for emode_unhealthy + collect_emode( + &mut emode_unhealthy, + &mut residual_deposit_value, + &mut borrow.borrow_weighted_value_usd, + ); + + // Pop element from basket of unweighted borrows if values + // are fully collected + if ( + eq(borrow.borrow_weighted_value_upper_bound_usd, decimal::from(0)) + && eq(borrow.borrow_weighted_value_usd, decimal::from(0)) + ) { + vector::pop_back(basket); + }; }; - i = i + 1; + len = len - 1; }; - obligation.unweighted_borrowed_value_usd = unweighted_borrowed_value_usd; - obligation.weighted_borrowed_value_usd = weighted_borrowed_value_usd; - obligation.weighted_borrowed_value_upper_bound_usd = weighted_borrowed_value_upper_bound_usd; + (some(emode_allowed), some(emode_unhealthy)) + } - obligation.borrowing_isolated_asset = borrowing_isolated_asset; + fun collect_emode( + emode_value: &mut Decimal, + residual_deposit_value: &mut Decimal, + borrow_weighted_value: &mut Decimal, + ) { + *emode_value = add( + *emode_value, + min(*borrow_weighted_value, *residual_deposit_value) + ); + + let previous_residual_deposit_value_lower_bound = *residual_deposit_value; + + *residual_deposit_value = saturating_sub( + *residual_deposit_value, + *borrow_weighted_value, + ); + + *borrow_weighted_value = saturating_sub( + *borrow_weighted_value, + previous_residual_deposit_value_lower_bound + ); + + } + + fun compute_allowed_borrow_value( + deposit_value_lower_bound: Decimal, + borrow_weighted_value_upper_bound_in_emode: Option, + config: &ReserveConfig, + open_ltv: Decimal, + open_ltv_emode: Decimal, + ): Decimal { + let open_ltv = open_ltv(config); + let net_deposit_value_lower_bound = deposit_value_lower_bound; + + let emode_value = if (is_some(&borrow_weighted_value_upper_bound_in_emode)) { + let borrow_weighted_value_upper_bound_in_emode = option::destroy_some(borrow_weighted_value_upper_bound_in_emode); + net_deposit_value_lower_bound = saturating_sub( + net_deposit_value_lower_bound, borrow_weighted_value_upper_bound_in_emode + ); + + mul( + borrow_weighted_value_upper_bound_in_emode, + open_ltv_emode, + ) + + } else { + decimal::from(0) + }; + + + let normal_value = mul( + net_deposit_value_lower_bound, + open_ltv + ); + + add( + emode_value, + normal_value, + ) } /// Process a deposit action diff --git a/contracts/sources/reserve.move b/contracts/sources/reserve.move index 2e11bb2..35420ba 100644 --- a/contracts/sources/reserve.move +++ b/contracts/sources/reserve.move @@ -486,6 +486,22 @@ module suilend::reserve { reserve_config::destroy(old); } + public(friend) fun set_emode_for_pair

( + reserve: &mut Reserve

, + reserve_array_index: u64, + open_ltv_pct: u8, + close_ltv_pct: u8, + ) { + let config = cell::get_mut(&mut reserve.config); + + reserve_config::set_emode_for_pair( + config, + reserve_array_index, + open_ltv_pct, + close_ltv_pct, + ); + } + public(friend) fun update_price

( reserve: &mut Reserve

, clock: &Clock, diff --git a/contracts/sources/reserve_config.move b/contracts/sources/reserve_config.move index 17cb742..6cbc043 100644 --- a/contracts/sources/reserve_config.move +++ b/contracts/sources/reserve_config.move @@ -1,16 +1,22 @@ /// parameters for a Reserve. module suilend::reserve_config { use std::vector::{Self}; + use std::option::{Option, some, none, is_none, fill, destroy_some}; use suilend::decimal::{Decimal, Self, add, sub, mul, div, ge, le}; use sui::tx_context::{TxContext}; use sui::bag::{Self, Bag}; + friend suilend::reserve; + friend suilend::obligation; + #[test_only] use sui::test_scenario::{Self}; const EInvalidReserveConfig: u64 = 0; const EInvalidUtil: u64 = 1; + struct EModeKey has copy, store, drop {} + struct ReserveConfig has store { // risk params open_ltv_pct: u8, @@ -57,6 +63,19 @@ module suilend::reserve_config { fields: Bag } + struct EmodeConfig has store { + // Corresponding to the deposited coin type + reserve_array_indices: vector, + correlated_pairs: vector + } + + struct EModeData has store, copy, drop { + // Corresponding to the correlated pair + reserve_array_index: u64, + open_ltv_pct: u8, + close_ltv_pct: u8, + } + public fun create_reserve_config( open_ltv_pct: u8, close_ltv_pct: u8, @@ -104,6 +123,98 @@ module suilend::reserve_config { config } + public(friend) fun set_emode_for_pair( + reserve_config: &mut ReserveConfig, + reserve_array_index: u64, + open_ltv_pct: u8, + close_ltv_pct: u8, + ) { + let has_emode_field = bag::contains(&reserve_config.additional_fields, EModeKey {}); + + if (!has_emode_field) { + bag::add( + &mut reserve_config.additional_fields, + EModeKey {}, + EmodeConfig { + reserve_array_indices: vector::empty(), + correlated_pairs: vector::empty(), + }, + ) + }; + + let emode_config: &mut EmodeConfig = bag::borrow_mut(&mut reserve_config.additional_fields, EModeKey {}); + + // Check if there is already emode parameters for the reserve_array_index + let pair_idx = get_pair_idx(emode_config, reserve_array_index); + + if (is_none(&pair_idx)) { + vector::push_back(&mut emode_config.reserve_array_indices, reserve_array_index); + vector::push_back(&mut emode_config.correlated_pairs, EModeData { + reserve_array_index, + open_ltv_pct, + close_ltv_pct, + }); + } else { + let pair_idx = destroy_some(pair_idx); + let emode_data = vector::borrow_mut(&mut emode_config.correlated_pairs, pair_idx); + emode_data.open_ltv_pct = open_ltv_pct; + emode_data.close_ltv_pct = close_ltv_pct; + }; + } + + public fun get_emode_config( + reserve_config: &ReserveConfig, + ): &EmodeConfig { + bag::borrow(&reserve_config.additional_fields, EModeKey {}) + } + + public fun has_emode_config( + reserve_config: &ReserveConfig, + ): bool { + bag::contains(&reserve_config.additional_fields, EModeKey {}) + } + + fun get_pair_idx( + emode_config: &EmodeConfig, + reserve_array_index: u64, + ): Option { + // Check if there is already emode parameters for the reserve_array_index + let i = vector::length(&emode_config.reserve_array_indices); + let pair_idx = none(); + + while (i > 0) { + let idx = vector::borrow(&emode_config.reserve_array_indices, i - 1); + + if (idx == &reserve_array_index) { + fill(&mut pair_idx, *idx); + }; + + i = i - 1; + }; + + pair_idx + } + + public(friend) fun is_correlated( + emode_config: &EmodeConfig, + reserve_array_index: u64, + ): bool { + // Check if there is already emode parameters for the reserve_array_index + let i = vector::length(&emode_config.reserve_array_indices); + + while (i > 0) { + let idx = vector::borrow(&emode_config.reserve_array_indices, i - 1); + + if (idx == &reserve_array_index) { + return true + }; + + i = i - 1; + }; + + false + } + fun validate_reserve_config(config: &ReserveConfig) { assert!(config.open_ltv_pct <= 100, EInvalidReserveConfig); assert!(config.close_ltv_pct <= 100, EInvalidReserveConfig); @@ -205,6 +316,17 @@ module suilend::reserve_config { decimal::from_bps(config.spread_fee_bps) } + // TODO + // public fun open_ltv_emode(config: &ReserveConfig): Decimal { + // decimal::from_percent( + // get_emode_config(config).open_ltv_pct + // ) + // } + + // public fun close_ltv_emode(config: &ReserveConfig): Decimal {x + // decimal::from_percent(config.close_ltv_pct) + // } + public fun calculate_apr(config: &ReserveConfig, cur_util: Decimal): Decimal { assert!(le(cur_util, decimal::from(1)), EInvalidUtil); @@ -267,6 +389,17 @@ module suilend::reserve_config { additional_fields } = config; + let has_emode_field = bag::contains(&additional_fields, EModeKey {}); + + if (has_emode_field) { + let emode_config: EmodeConfig = bag::remove( + &mut additional_fields, + EModeKey {}, + ); + + let EmodeConfig { reserve_array_indices: _, correlated_pairs: _ } = emode_config; + }; + bag::destroy_empty(additional_fields); } From 354f05ddc0985d47666efc2b7b185af22e2b5f6d Mon Sep 17 00:00:00 2001 From: 0xxgen1 <0xxgen@solend.fi> Date: Wed, 2 Oct 2024 12:21:12 +0100 Subject: [PATCH 2/9] eMode draft impl --- contracts/sources/obligation.move | 175 +++++++++++++++------- contracts/sources/reserve_config.move | 199 ++++++++++++-------------- 2 files changed, 214 insertions(+), 160 deletions(-) diff --git a/contracts/sources/obligation.move b/contracts/sources/obligation.move index edeb366..99f11dc 100644 --- a/contracts/sources/obligation.move +++ b/contracts/sources/obligation.move @@ -1,7 +1,7 @@ module suilend::obligation { // === Imports === use std::type_name::{TypeName, Self}; - use std::option::{Self, Option, some, none, is_some}; + use std::option::{Self, Option, some, none, is_some, is_none}; use sui::object::{Self, UID, ID}; use sui::balance::{Balance}; use std::vector::{Self}; @@ -170,7 +170,7 @@ module suilend::obligation { } } - struct UnweightedBorrow has copy, drop { + struct BorrowData has copy, drop { reserve_array_index: u64, borrow_weighted_value_usd: Decimal, borrow_weighted_value_upper_bound_usd: Decimal, @@ -190,7 +190,7 @@ module suilend::obligation { let weighted_borrowed_value_upper_bound_usd = decimal::from(0); let borrowing_isolated_asset = false; - let basket: vector = vector::empty(); + let borrow_data: vector = vector::empty(); while (i < vector::length(&obligation.borrows)) { let borrow = vector::borrow_mut(&mut obligation.borrows, i); @@ -230,7 +230,7 @@ module suilend::obligation { borrow_weighted_value_upper_bound_usd, ); - vector::push_back(&mut basket, UnweightedBorrow { + vector::push_back(&mut borrow_data, BorrowData { reserve_array_index: borrow.reserve_array_index, borrow_weighted_value_usd, borrow_weighted_value_upper_bound_usd, @@ -274,26 +274,40 @@ module suilend::obligation { deposit.market_value = market_value; deposited_value_usd = add(deposited_value_usd, market_value); - let (emode_allowed, emode_unhealthy) = collect_emode_values( + let ( + market_value_lower_bound_in_emode, + open_ltv_emode, + market_value_in_emode, + close_ltv_emode, + ) = compute_emode_deposits_and_ltvs( market_value, market_value_lower_bound, - &mut basket, + &mut borrow_data, config(deposit_reserve), ); + + let allowed_borrow_value_usd_i = compute_borrow_value_with_emode( + market_value_lower_bound, + open_ltv(config(deposit_reserve)), + market_value_lower_bound_in_emode, // borrow_weighted_value_upper_bound_in_emode + open_ltv_emode, + ); allowed_borrow_value_usd = add( allowed_borrow_value_usd, - mul( - market_value_lower_bound, - open_ltv(config(deposit_reserve)) - ) + allowed_borrow_value_usd_i, + ); + + let unhealthy_borrow_value_usd_i = compute_borrow_value_with_emode( + market_value, + close_ltv(config(deposit_reserve)), + market_value_in_emode, // borrow_weighted_value_in_emode + close_ltv_emode, ); + unhealthy_borrow_value_usd = add( unhealthy_borrow_value_usd, - mul( - market_value, - close_ltv(config(deposit_reserve)) - ) + unhealthy_borrow_value_usd_i, ); i = i + 1; @@ -304,73 +318,125 @@ module suilend::obligation { obligation.unhealthy_borrow_value_usd = unhealthy_borrow_value_usd; } - fun collect_emode_values( - deposit_value: Decimal, // vs. borrow_weighted_value - deposit_value_lower_bound: Decimal, // vs. borrow_weighted_value_upper_bound_usd - basket: &mut vector, + fun compute_emode_deposits_and_ltvs( + deposit_value: Decimal, + deposit_value_lower_bound: Decimal, + borrow_data: &mut vector, config: &ReserveConfig, - ): (Option, Option) { + ): ( + Option, // deposit_value_lower_bound_in_emode + Option, // open_ltv + Option, // deposit_value_in_emode + Option, // close_ltv + ) + { if (!reserve_config::has_emode_config(config)) { - return (none(), none()); + return (none(), none(), none(), none()) }; let emode_config = reserve_config::get_emode_config(config); - let len = vector::length(basket); - let emode_allowed = decimal::from(0); - let emode_unhealthy = decimal::from(0); + let len = vector::length(borrow_data); + + let deposit_value_lower_bound_in_emode = decimal::from(0); + let deposit_value_in_emode = decimal::from(0); + + let open_ltv = decimal::from(0); + let close_ltv = decimal::from(0); let residual_deposit_value = deposit_value; let residual_deposit_value_lower_bound = deposit_value_lower_bound; while (len > 0) { - let borrow = vector::borrow_mut(basket, len - 1); + let borrow = vector::borrow_mut(borrow_data, len - 1); - let is_correlated = reserve_config::is_correlated( + let (open_ltv_i, close_ltv_i) = reserve_config::get_ltvs( emode_config, borrow.reserve_array_index ); - if (!is_correlated) { + // open_ltv and close_ltv options are either both some or none + // so we only need to check one of them + if (is_none(&open_ltv_i)) { continue } else { - // Collect values for emode_allowed - collect_emode( - &mut emode_allowed, + // === Collect values for emode_allowed + let open_ltv_i = option::destroy_some(open_ltv_i); + let deposit_value_lower_bound_in_emode_before = deposit_value_lower_bound_in_emode; + + update_deposits_in_emode( + &mut deposit_value_lower_bound_in_emode, &mut residual_deposit_value_lower_bound, &mut borrow.borrow_weighted_value_upper_bound_usd, ); - // Collect values for emode_unhealthy - collect_emode( - &mut emode_unhealthy, + // Delta + let deposit_value_lower_bound_in_emode_i = sub( + deposit_value_lower_bound_in_emode, + deposit_value_lower_bound_in_emode_before + ); + + open_ltv = add( + open_ltv, + mul(deposit_value_lower_bound_in_emode_i, open_ltv_i) + ); + + + // === Collect values for emode_unhealthy + let close_ltv_i = option::destroy_some(close_ltv_i); + let deposit_value_in_emode_before = deposit_value_in_emode; + + update_deposits_in_emode( + &mut deposit_value_in_emode, &mut residual_deposit_value, &mut borrow.borrow_weighted_value_usd, ); + // Delta + let deposit_value_in_emode_i = sub(deposit_value_in_emode, deposit_value_in_emode_before); + close_ltv = add( + close_ltv, + mul(deposit_value_in_emode_i, close_ltv_i) + ); + // Pop element from basket of unweighted borrows if values // are fully collected if ( eq(borrow.borrow_weighted_value_upper_bound_usd, decimal::from(0)) && eq(borrow.borrow_weighted_value_usd, decimal::from(0)) ) { - vector::pop_back(basket); + vector::pop_back(borrow_data); }; }; len = len - 1; }; - (some(emode_allowed), some(emode_unhealthy)) + open_ltv = div( + open_ltv, + deposit_value_lower_bound_in_emode, + ); + + close_ltv = div( + close_ltv, + deposit_value_in_emode, + ); + + ( + some(deposit_value_lower_bound_in_emode), + some(open_ltv), + some(deposit_value_in_emode), + some(close_ltv), + ) } - fun collect_emode( - emode_value: &mut Decimal, + fun update_deposits_in_emode( + emode_deposit_value: &mut Decimal, residual_deposit_value: &mut Decimal, borrow_weighted_value: &mut Decimal, ) { - *emode_value = add( - *emode_value, + *emode_deposit_value = add( + *emode_deposit_value, min(*borrow_weighted_value, *residual_deposit_value) ); @@ -388,35 +454,34 @@ module suilend::obligation { } - fun compute_allowed_borrow_value( - deposit_value_lower_bound: Decimal, - borrow_weighted_value_upper_bound_in_emode: Option, - config: &ReserveConfig, - open_ltv: Decimal, - open_ltv_emode: Decimal, + fun compute_borrow_value_with_emode( + deposit_value_usd: Decimal, + ltv: Decimal, + deposit_value_usd_in_emode: Option, + ltv_emode: Option, ): Decimal { - let open_ltv = open_ltv(config); - let net_deposit_value_lower_bound = deposit_value_lower_bound; + let net_deposit_value_usd = deposit_value_usd; + + let emode_value = if (is_some(&deposit_value_usd_in_emode)) { + let deposit_value_usd_in_emode = option::destroy_some(deposit_value_usd_in_emode); + let ltv_emode = option::destroy_some(ltv_emode); - let emode_value = if (is_some(&borrow_weighted_value_upper_bound_in_emode)) { - let borrow_weighted_value_upper_bound_in_emode = option::destroy_some(borrow_weighted_value_upper_bound_in_emode); - net_deposit_value_lower_bound = saturating_sub( - net_deposit_value_lower_bound, borrow_weighted_value_upper_bound_in_emode + net_deposit_value_usd = saturating_sub( + net_deposit_value_usd, deposit_value_usd_in_emode ); mul( - borrow_weighted_value_upper_bound_in_emode, - open_ltv_emode, + deposit_value_usd_in_emode, + ltv_emode, ) } else { decimal::from(0) }; - let normal_value = mul( - net_deposit_value_lower_bound, - open_ltv + net_deposit_value_usd, + ltv ); add( diff --git a/contracts/sources/reserve_config.move b/contracts/sources/reserve_config.move index 6cbc043..3b85781 100644 --- a/contracts/sources/reserve_config.move +++ b/contracts/sources/reserve_config.move @@ -66,7 +66,7 @@ module suilend::reserve_config { struct EmodeConfig has store { // Corresponding to the deposited coin type reserve_array_indices: vector, - correlated_pairs: vector + emode_pairs: vector } struct EModeData has store, copy, drop { @@ -123,98 +123,6 @@ module suilend::reserve_config { config } - public(friend) fun set_emode_for_pair( - reserve_config: &mut ReserveConfig, - reserve_array_index: u64, - open_ltv_pct: u8, - close_ltv_pct: u8, - ) { - let has_emode_field = bag::contains(&reserve_config.additional_fields, EModeKey {}); - - if (!has_emode_field) { - bag::add( - &mut reserve_config.additional_fields, - EModeKey {}, - EmodeConfig { - reserve_array_indices: vector::empty(), - correlated_pairs: vector::empty(), - }, - ) - }; - - let emode_config: &mut EmodeConfig = bag::borrow_mut(&mut reserve_config.additional_fields, EModeKey {}); - - // Check if there is already emode parameters for the reserve_array_index - let pair_idx = get_pair_idx(emode_config, reserve_array_index); - - if (is_none(&pair_idx)) { - vector::push_back(&mut emode_config.reserve_array_indices, reserve_array_index); - vector::push_back(&mut emode_config.correlated_pairs, EModeData { - reserve_array_index, - open_ltv_pct, - close_ltv_pct, - }); - } else { - let pair_idx = destroy_some(pair_idx); - let emode_data = vector::borrow_mut(&mut emode_config.correlated_pairs, pair_idx); - emode_data.open_ltv_pct = open_ltv_pct; - emode_data.close_ltv_pct = close_ltv_pct; - }; - } - - public fun get_emode_config( - reserve_config: &ReserveConfig, - ): &EmodeConfig { - bag::borrow(&reserve_config.additional_fields, EModeKey {}) - } - - public fun has_emode_config( - reserve_config: &ReserveConfig, - ): bool { - bag::contains(&reserve_config.additional_fields, EModeKey {}) - } - - fun get_pair_idx( - emode_config: &EmodeConfig, - reserve_array_index: u64, - ): Option { - // Check if there is already emode parameters for the reserve_array_index - let i = vector::length(&emode_config.reserve_array_indices); - let pair_idx = none(); - - while (i > 0) { - let idx = vector::borrow(&emode_config.reserve_array_indices, i - 1); - - if (idx == &reserve_array_index) { - fill(&mut pair_idx, *idx); - }; - - i = i - 1; - }; - - pair_idx - } - - public(friend) fun is_correlated( - emode_config: &EmodeConfig, - reserve_array_index: u64, - ): bool { - // Check if there is already emode parameters for the reserve_array_index - let i = vector::length(&emode_config.reserve_array_indices); - - while (i > 0) { - let idx = vector::borrow(&emode_config.reserve_array_indices, i - 1); - - if (idx == &reserve_array_index) { - return true - }; - - i = i - 1; - }; - - false - } - fun validate_reserve_config(config: &ReserveConfig) { assert!(config.open_ltv_pct <= 100, EInvalidReserveConfig); assert!(config.close_ltv_pct <= 100, EInvalidReserveConfig); @@ -316,17 +224,6 @@ module suilend::reserve_config { decimal::from_bps(config.spread_fee_bps) } - // TODO - // public fun open_ltv_emode(config: &ReserveConfig): Decimal { - // decimal::from_percent( - // get_emode_config(config).open_ltv_pct - // ) - // } - - // public fun close_ltv_emode(config: &ReserveConfig): Decimal {x - // decimal::from_percent(config.close_ltv_pct) - // } - public fun calculate_apr(config: &ReserveConfig, cur_util: Decimal): Decimal { assert!(le(cur_util, decimal::from(1)), EInvalidUtil); @@ -397,7 +294,7 @@ module suilend::reserve_config { EModeKey {}, ); - let EmodeConfig { reserve_array_indices: _, correlated_pairs: _ } = emode_config; + let EmodeConfig { reserve_array_indices: _, emode_pairs: _ } = emode_config; }; bag::destroy_empty(additional_fields); @@ -539,6 +436,98 @@ module suilend::reserve_config { } + // === eMode Package Functions == + + public(friend) fun set_emode_for_pair( + reserve_config: &mut ReserveConfig, + reserve_array_index: u64, + open_ltv_pct: u8, + close_ltv_pct: u8, + ) { + let has_emode_field = bag::contains(&reserve_config.additional_fields, EModeKey {}); + + if (!has_emode_field) { + bag::add( + &mut reserve_config.additional_fields, + EModeKey {}, + EmodeConfig { + reserve_array_indices: vector::empty(), + emode_pairs: vector::empty(), + }, + ) + }; + + let emode_config: &mut EmodeConfig = bag::borrow_mut(&mut reserve_config.additional_fields, EModeKey {}); + + // Check if there is already emode parameters for the reserve_array_index + let pair_idx = get_pair_idx(emode_config, reserve_array_index); + + if (is_none(&pair_idx)) { + vector::push_back(&mut emode_config.reserve_array_indices, reserve_array_index); + vector::push_back(&mut emode_config.emode_pairs, EModeData { + reserve_array_index, + open_ltv_pct, + close_ltv_pct, + }); + } else { + let pair_idx = destroy_some(pair_idx); + let emode_data = vector::borrow_mut(&mut emode_config.emode_pairs, pair_idx); + emode_data.open_ltv_pct = open_ltv_pct; + emode_data.close_ltv_pct = close_ltv_pct; + }; + } + + public(friend) fun get_emode_config( + reserve_config: &ReserveConfig, + ): &EmodeConfig { + bag::borrow(&reserve_config.additional_fields, EModeKey {}) + } + + public(friend) fun has_emode_config( + reserve_config: &ReserveConfig, + ): bool { + bag::contains(&reserve_config.additional_fields, EModeKey {}) + } + + fun get_pair_idx( + emode_config: &EmodeConfig, + reserve_array_index: u64, + ): Option { + // Check if there is already emode parameters for the reserve_array_index + let i = vector::length(&emode_config.reserve_array_indices); + let pair_idx = none(); + + while (i > 0) { + let idx = vector::borrow(&emode_config.reserve_array_indices, i - 1); + + if (idx == &reserve_array_index) { + fill(&mut pair_idx, *idx); + }; + + i = i - 1; + }; + + pair_idx + } + + public(friend) fun get_ltvs( + emode_config: &EmodeConfig, + reserve_array_index: u64, + ): (Option, Option) { + let pair_idx = get_pair_idx(emode_config, reserve_array_index); + + if (is_none(&pair_idx)) { + (none(), none()) + } else { + let pair_idx = destroy_some(pair_idx); + let emode_data = vector::borrow(&emode_config.emode_pairs, pair_idx); + ( + some(decimal::from_percent(emode_data.open_ltv_pct)), + some(decimal::from_percent(emode_data.close_ltv_pct)) + ) + } + } + // === Tests == #[test] fun test_calculate_apr() { From ef586b5b0da124143451f73ba7c676edebe702ca Mon Sep 17 00:00:00 2001 From: 0xxgen1 <0xxgen@solend.fi> Date: Wed, 2 Oct 2024 18:06:24 +0100 Subject: [PATCH 3/9] Use VecMap to store emode ltvs --- contracts/sources/obligation.move | 3 +- contracts/sources/reserve_config.move | 69 ++++++++------------------- 2 files changed, 22 insertions(+), 50 deletions(-) diff --git a/contracts/sources/obligation.move b/contracts/sources/obligation.move index 99f11dc..c8e1e3c 100644 --- a/contracts/sources/obligation.move +++ b/contracts/sources/obligation.move @@ -11,7 +11,6 @@ module suilend::obligation { use suilend::reserve::{Self, Reserve, config}; use suilend::reserve_config::{ ReserveConfig, - EmodeConfig, open_ltv, close_ltv, borrow_weight, @@ -350,7 +349,7 @@ module suilend::obligation { while (len > 0) { let borrow = vector::borrow_mut(borrow_data, len - 1); - let (open_ltv_i, close_ltv_i) = reserve_config::get_ltvs( + let (open_ltv_i, close_ltv_i) = reserve_config::get_emode_ltvs( emode_config, borrow.reserve_array_index ); diff --git a/contracts/sources/reserve_config.move b/contracts/sources/reserve_config.move index 3b85781..79e8eca 100644 --- a/contracts/sources/reserve_config.move +++ b/contracts/sources/reserve_config.move @@ -1,10 +1,11 @@ /// parameters for a Reserve. module suilend::reserve_config { use std::vector::{Self}; - use std::option::{Option, some, none, is_none, fill, destroy_some}; + use std::option::{Option, some, none}; use suilend::decimal::{Decimal, Self, add, sub, mul, div, ge, le}; use sui::tx_context::{TxContext}; use sui::bag::{Self, Bag}; + use sui::vec_map::{Self, VecMap}; friend suilend::reserve; friend suilend::obligation; @@ -63,11 +64,11 @@ module suilend::reserve_config { fields: Bag } - struct EmodeConfig has store { - // Corresponding to the deposited coin type - reserve_array_indices: vector, - emode_pairs: vector - } + // struct EmodeConfig has store { + // // Corresponding to the deposited coin type + // reserve_array_indices: vector, + // emode_pairs: vector + // } struct EModeData has store, copy, drop { // Corresponding to the correlated pair @@ -289,12 +290,10 @@ module suilend::reserve_config { let has_emode_field = bag::contains(&additional_fields, EModeKey {}); if (has_emode_field) { - let emode_config: EmodeConfig = bag::remove( + let _emode_config: VecMap = bag::remove( &mut additional_fields, EModeKey {}, ); - - let EmodeConfig { reserve_array_indices: _, emode_pairs: _ } = emode_config; }; bag::destroy_empty(additional_fields); @@ -450,28 +449,24 @@ module suilend::reserve_config { bag::add( &mut reserve_config.additional_fields, EModeKey {}, - EmodeConfig { - reserve_array_indices: vector::empty(), - emode_pairs: vector::empty(), - }, + vec_map::empty(), ) }; - let emode_config: &mut EmodeConfig = bag::borrow_mut(&mut reserve_config.additional_fields, EModeKey {}); + let emode_config: &mut VecMap = bag::borrow_mut(&mut reserve_config.additional_fields, EModeKey {}); // Check if there is already emode parameters for the reserve_array_index - let pair_idx = get_pair_idx(emode_config, reserve_array_index); + let has_pair = vec_map::contains(emode_config, &reserve_array_index); - if (is_none(&pair_idx)) { - vector::push_back(&mut emode_config.reserve_array_indices, reserve_array_index); - vector::push_back(&mut emode_config.emode_pairs, EModeData { + if (!has_pair) { + vec_map::insert(emode_config, reserve_array_index, EModeData { reserve_array_index, open_ltv_pct, close_ltv_pct, }); } else { - let pair_idx = destroy_some(pair_idx); - let emode_data = vector::borrow_mut(&mut emode_config.emode_pairs, pair_idx); + let emode_data = vec_map::get_mut(emode_config, &reserve_array_index); + emode_data.open_ltv_pct = open_ltv_pct; emode_data.close_ltv_pct = close_ltv_pct; }; @@ -479,7 +474,7 @@ module suilend::reserve_config { public(friend) fun get_emode_config( reserve_config: &ReserveConfig, - ): &EmodeConfig { + ): &VecMap { bag::borrow(&reserve_config.additional_fields, EModeKey {}) } @@ -488,39 +483,17 @@ module suilend::reserve_config { ): bool { bag::contains(&reserve_config.additional_fields, EModeKey {}) } - - fun get_pair_idx( - emode_config: &EmodeConfig, - reserve_array_index: u64, - ): Option { - // Check if there is already emode parameters for the reserve_array_index - let i = vector::length(&emode_config.reserve_array_indices); - let pair_idx = none(); - - while (i > 0) { - let idx = vector::borrow(&emode_config.reserve_array_indices, i - 1); - - if (idx == &reserve_array_index) { - fill(&mut pair_idx, *idx); - }; - - i = i - 1; - }; - - pair_idx - } - public(friend) fun get_ltvs( - emode_config: &EmodeConfig, + public(friend) fun get_emode_ltvs( + emode_config: &VecMap, reserve_array_index: u64, ): (Option, Option) { - let pair_idx = get_pair_idx(emode_config, reserve_array_index); + let has_pair = vec_map::contains(emode_config, &reserve_array_index); - if (is_none(&pair_idx)) { + if (!has_pair) { (none(), none()) } else { - let pair_idx = destroy_some(pair_idx); - let emode_data = vector::borrow(&emode_config.emode_pairs, pair_idx); + let emode_data = vec_map::get(emode_config, &reserve_array_index); ( some(decimal::from_percent(emode_data.open_ltv_pct)), some(decimal::from_percent(emode_data.close_ltv_pct)) From a5430d488a76e056466c41e292122dc1184e378e Mon Sep 17 00:00:00 2001 From: 0xxgen1 <0xxgen@solend.fi> Date: Wed, 2 Oct 2024 18:32:25 +0100 Subject: [PATCH 4/9] Remove emode cross-margin --- contracts/sources/obligation.move | 327 ++++++-------------------- contracts/sources/reserve_config.move | 34 +-- 2 files changed, 91 insertions(+), 270 deletions(-) diff --git a/contracts/sources/obligation.move b/contracts/sources/obligation.move index c8e1e3c..20548c0 100644 --- a/contracts/sources/obligation.move +++ b/contracts/sources/obligation.move @@ -1,22 +1,22 @@ module suilend::obligation { // === Imports === use std::type_name::{TypeName, Self}; - use std::option::{Self, Option, some, none, is_some, is_none}; use sui::object::{Self, UID, ID}; use sui::balance::{Balance}; use std::vector::{Self}; - use sui::vec_map::{Self}; + use sui::dynamic_field::{Self as df}; use sui::event::{Self}; use sui::tx_context::{TxContext}; use suilend::reserve::{Self, Reserve, config}; use suilend::reserve_config::{ - ReserveConfig, open_ltv, close_ltv, borrow_weight, liquidation_bonus, protocol_liquidation_fee, isolated, + open_ltv_emode, + close_ltv_emode, }; use sui::clock::{Clock}; use suilend::decimal::{Self, Decimal, mul, add, sub, div, gt, lt, min, floor, le, eq, saturating_sub}; @@ -47,6 +47,8 @@ module suilend::obligation { const MAX_DEPOSITS: u64 = 5; const MAX_BORROWS: u64 = 5; + struct EModeFlag has store, copy, drop {} + // === Structs === struct Obligation has key, store { id: UID, @@ -169,13 +171,10 @@ module suilend::obligation { } } - struct BorrowData has copy, drop { - reserve_array_index: u64, - borrow_weighted_value_usd: Decimal, - borrow_weighted_value_upper_bound_usd: Decimal, + fun is_emode

(obligation: &Obligation

): bool { + df::exists_(&obligation.id, EModeFlag {}) } - /// update the obligation's borrowed amounts and health values. this is /// called by the lending market prior to any borrow, withdraw, or liquidate operation. public(friend) fun refresh

( @@ -183,14 +182,76 @@ module suilend::obligation { reserves: &mut vector>, clock: &Clock ) { + let i = 0; + let deposited_value_usd = decimal::from(0); + let allowed_borrow_value_usd = decimal::from(0); + let unhealthy_borrow_value_usd = decimal::from(0); + let is_emode = is_emode(obligation); + + while (i < vector::length(&obligation.deposits)) { + let deposit = vector::borrow_mut(&mut obligation.deposits, i); + + let deposit_reserve = vector::borrow_mut(reserves, deposit.reserve_array_index); + + reserve::compound_interest(deposit_reserve, clock); + reserve::assert_price_is_fresh(deposit_reserve, clock); + + let market_value = reserve::ctoken_market_value( + deposit_reserve, + deposit.deposited_ctoken_amount + ); + let market_value_lower_bound = reserve::ctoken_market_value_lower_bound( + deposit_reserve, + deposit.deposited_ctoken_amount + ); + + deposit.market_value = market_value; + deposited_value_usd = add(deposited_value_usd, market_value); + + let (open_ltv, close_ltv) = if (is_emode) { + // There is only one borrow in such circunstance, hence the indexing to 0 + let borrow_reserve_array_index = vector::borrow(&obligation.borrows, 0).reserve_array_index; + + let emode_data = reserve_config::get_emode_ltvs( + config(deposit_reserve), + &borrow_reserve_array_index + ); + + (open_ltv_emode(emode_data), close_ltv_emode(emode_data)) + } else { + (open_ltv(config(deposit_reserve)), close_ltv(config(deposit_reserve))) + }; + + allowed_borrow_value_usd = add( + allowed_borrow_value_usd, + mul( + market_value_lower_bound, + open_ltv, + ), + ); + + unhealthy_borrow_value_usd = add( + unhealthy_borrow_value_usd, + mul( + market_value, + close_ltv, + ), + ); + + i = i + 1; + }; + + obligation.deposited_value_usd = deposited_value_usd; + obligation.allowed_borrow_value_usd = allowed_borrow_value_usd; + obligation.unhealthy_borrow_value_usd = unhealthy_borrow_value_usd; + + let i = 0; let unweighted_borrowed_value_usd = decimal::from(0); let weighted_borrowed_value_usd = decimal::from(0); let weighted_borrowed_value_upper_bound_usd = decimal::from(0); let borrowing_isolated_asset = false; - let borrow_data: vector = vector::empty(); - while (i < vector::length(&obligation.borrows)) { let borrow = vector::borrow_mut(&mut obligation.borrows, i); @@ -229,12 +290,6 @@ module suilend::obligation { borrow_weighted_value_upper_bound_usd, ); - vector::push_back(&mut borrow_data, BorrowData { - reserve_array_index: borrow.reserve_array_index, - borrow_weighted_value_usd, - borrow_weighted_value_upper_bound_usd, - }); - if (isolated(config(borrow_reserve))) { borrowing_isolated_asset = true; }; @@ -247,246 +302,6 @@ module suilend::obligation { obligation.weighted_borrowed_value_upper_bound_usd = weighted_borrowed_value_upper_bound_usd; obligation.borrowing_isolated_asset = borrowing_isolated_asset; - - let i = 0; - let deposited_value_usd = decimal::from(0); - let allowed_borrow_value_usd = decimal::from(0); - let unhealthy_borrow_value_usd = decimal::from(0); - - while (i < vector::length(&obligation.deposits)) { - let deposit = vector::borrow_mut(&mut obligation.deposits, i); - - let deposit_reserve = vector::borrow_mut(reserves, deposit.reserve_array_index); - - reserve::compound_interest(deposit_reserve, clock); - reserve::assert_price_is_fresh(deposit_reserve, clock); - - let market_value = reserve::ctoken_market_value( - deposit_reserve, - deposit.deposited_ctoken_amount - ); - let market_value_lower_bound = reserve::ctoken_market_value_lower_bound( - deposit_reserve, - deposit.deposited_ctoken_amount - ); - - deposit.market_value = market_value; - deposited_value_usd = add(deposited_value_usd, market_value); - - let ( - market_value_lower_bound_in_emode, - open_ltv_emode, - market_value_in_emode, - close_ltv_emode, - ) = compute_emode_deposits_and_ltvs( - market_value, - market_value_lower_bound, - &mut borrow_data, - config(deposit_reserve), - ); - - let allowed_borrow_value_usd_i = compute_borrow_value_with_emode( - market_value_lower_bound, - open_ltv(config(deposit_reserve)), - market_value_lower_bound_in_emode, // borrow_weighted_value_upper_bound_in_emode - open_ltv_emode, - ); - - allowed_borrow_value_usd = add( - allowed_borrow_value_usd, - allowed_borrow_value_usd_i, - ); - - let unhealthy_borrow_value_usd_i = compute_borrow_value_with_emode( - market_value, - close_ltv(config(deposit_reserve)), - market_value_in_emode, // borrow_weighted_value_in_emode - close_ltv_emode, - ); - - unhealthy_borrow_value_usd = add( - unhealthy_borrow_value_usd, - unhealthy_borrow_value_usd_i, - ); - - i = i + 1; - }; - - obligation.deposited_value_usd = deposited_value_usd; - obligation.allowed_borrow_value_usd = allowed_borrow_value_usd; - obligation.unhealthy_borrow_value_usd = unhealthy_borrow_value_usd; - } - - fun compute_emode_deposits_and_ltvs( - deposit_value: Decimal, - deposit_value_lower_bound: Decimal, - borrow_data: &mut vector, - config: &ReserveConfig, - ): ( - Option, // deposit_value_lower_bound_in_emode - Option, // open_ltv - Option, // deposit_value_in_emode - Option, // close_ltv - ) - { - if (!reserve_config::has_emode_config(config)) { - return (none(), none(), none(), none()) - }; - - let emode_config = reserve_config::get_emode_config(config); - - let len = vector::length(borrow_data); - - let deposit_value_lower_bound_in_emode = decimal::from(0); - let deposit_value_in_emode = decimal::from(0); - - let open_ltv = decimal::from(0); - let close_ltv = decimal::from(0); - - let residual_deposit_value = deposit_value; - let residual_deposit_value_lower_bound = deposit_value_lower_bound; - - while (len > 0) { - let borrow = vector::borrow_mut(borrow_data, len - 1); - - let (open_ltv_i, close_ltv_i) = reserve_config::get_emode_ltvs( - emode_config, - borrow.reserve_array_index - ); - - // open_ltv and close_ltv options are either both some or none - // so we only need to check one of them - if (is_none(&open_ltv_i)) { - continue - } else { - // === Collect values for emode_allowed - let open_ltv_i = option::destroy_some(open_ltv_i); - let deposit_value_lower_bound_in_emode_before = deposit_value_lower_bound_in_emode; - - update_deposits_in_emode( - &mut deposit_value_lower_bound_in_emode, - &mut residual_deposit_value_lower_bound, - &mut borrow.borrow_weighted_value_upper_bound_usd, - ); - - // Delta - let deposit_value_lower_bound_in_emode_i = sub( - deposit_value_lower_bound_in_emode, - deposit_value_lower_bound_in_emode_before - ); - - open_ltv = add( - open_ltv, - mul(deposit_value_lower_bound_in_emode_i, open_ltv_i) - ); - - - // === Collect values for emode_unhealthy - let close_ltv_i = option::destroy_some(close_ltv_i); - let deposit_value_in_emode_before = deposit_value_in_emode; - - update_deposits_in_emode( - &mut deposit_value_in_emode, - &mut residual_deposit_value, - &mut borrow.borrow_weighted_value_usd, - ); - - // Delta - let deposit_value_in_emode_i = sub(deposit_value_in_emode, deposit_value_in_emode_before); - close_ltv = add( - close_ltv, - mul(deposit_value_in_emode_i, close_ltv_i) - ); - - // Pop element from basket of unweighted borrows if values - // are fully collected - if ( - eq(borrow.borrow_weighted_value_upper_bound_usd, decimal::from(0)) - && eq(borrow.borrow_weighted_value_usd, decimal::from(0)) - ) { - vector::pop_back(borrow_data); - }; - }; - - len = len - 1; - }; - - open_ltv = div( - open_ltv, - deposit_value_lower_bound_in_emode, - ); - - close_ltv = div( - close_ltv, - deposit_value_in_emode, - ); - - ( - some(deposit_value_lower_bound_in_emode), - some(open_ltv), - some(deposit_value_in_emode), - some(close_ltv), - ) - } - - fun update_deposits_in_emode( - emode_deposit_value: &mut Decimal, - residual_deposit_value: &mut Decimal, - borrow_weighted_value: &mut Decimal, - ) { - *emode_deposit_value = add( - *emode_deposit_value, - min(*borrow_weighted_value, *residual_deposit_value) - ); - - let previous_residual_deposit_value_lower_bound = *residual_deposit_value; - - *residual_deposit_value = saturating_sub( - *residual_deposit_value, - *borrow_weighted_value, - ); - - *borrow_weighted_value = saturating_sub( - *borrow_weighted_value, - previous_residual_deposit_value_lower_bound - ); - - } - - fun compute_borrow_value_with_emode( - deposit_value_usd: Decimal, - ltv: Decimal, - deposit_value_usd_in_emode: Option, - ltv_emode: Option, - ): Decimal { - let net_deposit_value_usd = deposit_value_usd; - - let emode_value = if (is_some(&deposit_value_usd_in_emode)) { - let deposit_value_usd_in_emode = option::destroy_some(deposit_value_usd_in_emode); - let ltv_emode = option::destroy_some(ltv_emode); - - net_deposit_value_usd = saturating_sub( - net_deposit_value_usd, deposit_value_usd_in_emode - ); - - mul( - deposit_value_usd_in_emode, - ltv_emode, - ) - - } else { - decimal::from(0) - }; - - let normal_value = mul( - net_deposit_value_usd, - ltv - ); - - add( - emode_value, - normal_value, - ) } /// Process a deposit action diff --git a/contracts/sources/reserve_config.move b/contracts/sources/reserve_config.move index 79e8eca..3d85ee1 100644 --- a/contracts/sources/reserve_config.move +++ b/contracts/sources/reserve_config.move @@ -1,7 +1,6 @@ /// parameters for a Reserve. module suilend::reserve_config { use std::vector::{Self}; - use std::option::{Option, some, none}; use suilend::decimal::{Decimal, Self, add, sub, mul, div, ge, le}; use sui::tx_context::{TxContext}; use sui::bag::{Self, Bag}; @@ -483,22 +482,29 @@ module suilend::reserve_config { ): bool { bag::contains(&reserve_config.additional_fields, EModeKey {}) } + + public(friend) fun open_ltv_emode( + emode_data: &EModeData, + ): Decimal { + decimal::from_percent(emode_data.open_ltv_pct) + } + + public(friend) fun close_ltv_emode( + emode_data: &EModeData, + ): Decimal { + decimal::from_percent(emode_data.open_ltv_pct) + } public(friend) fun get_emode_ltvs( - emode_config: &VecMap, - reserve_array_index: u64, - ): (Option, Option) { - let has_pair = vec_map::contains(emode_config, &reserve_array_index); + reserve_config: &ReserveConfig, + reserve_array_index: &u64, + ): &EModeData { + let emode_config = get_emode_config(reserve_config); + let has_pair = vec_map::contains(emode_config, reserve_array_index); - if (!has_pair) { - (none(), none()) - } else { - let emode_data = vec_map::get(emode_config, &reserve_array_index); - ( - some(decimal::from_percent(emode_data.open_ltv_pct)), - some(decimal::from_percent(emode_data.close_ltv_pct)) - ) - } + assert!(has_pair, 0); + + vec_map::get(emode_config, reserve_array_index) } // === Tests == From 1aed49b13f6b4134788dfd658524debb70d74d09 Mon Sep 17 00:00:00 2001 From: 0xxgen1 <0xxgen@solend.fi> Date: Thu, 3 Oct 2024 18:43:12 +0100 Subject: [PATCH 5/9] - Refactored obligation methods borrow, repay, deposit, withdraw to account for emode - Public function to toggle emode for a given obligation --- contracts/sources/lending_market.move | 25 +++- contracts/sources/obligation.move | 162 +++++++++++++++++++++----- contracts/sources/reserve_config.move | 16 +-- 3 files changed, 166 insertions(+), 37 deletions(-) diff --git a/contracts/sources/lending_market.move b/contracts/sources/lending_market.move index beaea94..c06c67c 100644 --- a/contracts/sources/lending_market.move +++ b/contracts/sources/lending_market.move @@ -379,6 +379,30 @@ module suilend::lending_market { obligation::zero_out_rewards_if_looped(obligation, &mut lending_market.reserves, clock); coin::from_balance(receive_balance, ctx) } + + /// Set emode for obligation - T is the deposit coin type + public fun set_emode( + lending_market: &mut LendingMarket

, + deposit_reserve_array_index: u64, + borrow_reserve_array_index: u64, + obligation_owner_cap: &ObligationOwnerCap

, + ) { + assert!(lending_market.version == CURRENT_VERSION, EIncorrectVersion); + + let obligation = object_table::borrow_mut( + &mut lending_market.obligations, + obligation_owner_cap.obligation_id + ); + + let deposit_reserve = vector::borrow(&lending_market.reserves, deposit_reserve_array_index); + assert!(reserve::coin_type(deposit_reserve) == type_name::get(), EWrongType); + + obligation::set_emode( + obligation, + deposit_reserve, + borrow_reserve_array_index + ); + } public fun withdraw_ctokens( lending_market: &mut LendingMarket

, @@ -819,7 +843,6 @@ module suilend::lending_market { reserve::update_reserve_config

(reserve, config); } - // TODO:Consider taking EModeConfig as param.. public fun set_emode_for_pair( _: &LendingMarketOwnerCap

, lending_market: &mut LendingMarket

, diff --git a/contracts/sources/obligation.move b/contracts/sources/obligation.move index 20548c0..3d7e4c1 100644 --- a/contracts/sources/obligation.move +++ b/contracts/sources/obligation.move @@ -41,12 +41,16 @@ module suilend::obligation { const ETooManyBorrows: u64 = 6; const EObligationIsNotForgivable: u64 = 7; const ECannotDepositAndBorrowSameAsset: u64 = 8; + const EEmodeInvalidForPairProvided: u64 = 9; + const EInvalidEModeDeposit: u64 = 10; + const EInvalidEModeBorrow: u64 = 11; // === Constants === const CLOSE_FACTOR_PCT: u8 = 20; const MAX_DEPOSITS: u64 = 5; const MAX_BORROWS: u64 = 5; + // === Dynamic Field Keys === struct EModeFlag has store, copy, drop {} // === Structs === @@ -109,6 +113,11 @@ module suilend::obligation { user_reward_manager_index: u64 } + struct EModeReserveIndices has store { + deposit_reserve_array_index: u64, + borrow_reserve_array_index: u64 + } + // === Events === struct ObligationDataEvent has drop, copy { lending_market_id: address, @@ -171,8 +180,23 @@ module suilend::obligation { } } - fun is_emode

(obligation: &Obligation

): bool { - df::exists_(&obligation.id, EModeFlag {}) + public(friend) fun set_emode

( + obligation: &mut Obligation

, + deposit_reserve: &Reserve

, + borrow_reserve_array_index: u64, + ) { + let deposit_reserve_array_index = reserve::array_index(deposit_reserve); + + let is_valid_emode = reserve_config::check_emode_validity( + config(deposit_reserve), &borrow_reserve_array_index + ); + + assert!(is_valid_emode, EEmodeInvalidForPairProvided); + + df::add(&mut obligation.id, EModeFlag {}, EModeReserveIndices { + deposit_reserve_array_index, + borrow_reserve_array_index, + }); } /// update the obligation's borrowed amounts and health values. this is @@ -208,19 +232,11 @@ module suilend::obligation { deposit.market_value = market_value; deposited_value_usd = add(deposited_value_usd, market_value); - let (open_ltv, close_ltv) = if (is_emode) { - // There is only one borrow in such circunstance, hence the indexing to 0 - let borrow_reserve_array_index = vector::borrow(&obligation.borrows, 0).reserve_array_index; - - let emode_data = reserve_config::get_emode_ltvs( - config(deposit_reserve), - &borrow_reserve_array_index - ); - - (open_ltv_emode(emode_data), close_ltv_emode(emode_data)) - } else { - (open_ltv(config(deposit_reserve)), close_ltv(config(deposit_reserve))) - }; + let (open_ltv, close_ltv) = get_ltvs( + obligation, + deposit_reserve, + is_emode, + ); allowed_borrow_value_usd = add( allowed_borrow_value_usd, @@ -270,9 +286,15 @@ module suilend::obligation { borrow.market_value = market_value; unweighted_borrowed_value_usd = add(unweighted_borrowed_value_usd, market_value); + let borrow_weight = if (is_emode) { + decimal::from(1) + } else { + borrow_weight(config(borrow_reserve)) + }; + let borrow_weighted_value_usd = mul( market_value, - borrow_weight(config(borrow_reserve)) + borrow_weight ); weighted_borrowed_value_usd = add( @@ -282,7 +304,7 @@ module suilend::obligation { let borrow_weighted_value_upper_bound_usd = mul( market_value_upper_bound, - borrow_weight(config(borrow_reserve)) + borrow_weight ); weighted_borrowed_value_upper_bound_usd = add( @@ -312,11 +334,27 @@ module suilend::obligation { ctoken_amount: u64, ) { let deposit_index = find_or_add_deposit(obligation, reserve, clock); - assert!(vector::length(&obligation.deposits) <= MAX_DEPOSITS, ETooManyDeposits); + let is_emode = is_emode(obligation); + + if (is_emode) { + assert!(vector::length(&obligation.deposits) == 1, ETooManyDeposits); + + let target_reserve_index = emode_deposit_reserve_array_index(obligation); + let deposit = vector::borrow(&obligation.deposits, deposit_index); + assert!(deposit.reserve_array_index == target_reserve_index, EInvalidEModeDeposit); + } else { + assert!(vector::length(&obligation.deposits) <= MAX_DEPOSITS, ETooManyDeposits); + }; let borrow_index = find_borrow_index(obligation, reserve); assert!(borrow_index == vector::length(&obligation.borrows), ECannotDepositAndBorrowSameAsset); + let (open_ltv, close_ltv) = get_ltvs( + obligation, + reserve, + is_emode, + ); + let deposit = vector::borrow_mut(&mut obligation.deposits, deposit_index); deposit.deposited_ctoken_amount = deposit.deposited_ctoken_amount + ctoken_amount; @@ -332,14 +370,14 @@ module suilend::obligation { obligation.allowed_borrow_value_usd, mul( reserve::ctoken_market_value_lower_bound(reserve, ctoken_amount), - open_ltv(config(reserve)) + open_ltv, ) ); obligation.unhealthy_borrow_value_usd = add( obligation.unhealthy_borrow_value_usd, mul( deposit_value, - close_ltv(config(reserve)) + close_ltv, ) ); @@ -361,7 +399,16 @@ module suilend::obligation { amount: u64, ) { let borrow_index = find_or_add_borrow(obligation, reserve, clock); - assert!(vector::length(&obligation.borrows) <= MAX_BORROWS, ETooManyBorrows); + let is_emode = is_emode(obligation); + + if (is_emode) { + assert!(vector::length(&obligation.borrows) == 1, ETooManyBorrows); + let target_reserve_index = emode_borrow_reserve_array_index(obligation); + let borrow = vector::borrow(&obligation.borrows, borrow_index); + assert!(borrow.reserve_array_index == target_reserve_index, EInvalidEModeBorrow); + } else { + assert!(vector::length(&obligation.borrows) <= MAX_BORROWS, ETooManyBorrows); + }; let deposit_index = find_deposit_index(obligation, reserve); assert!(deposit_index == vector::length(&obligation.deposits), ECannotDepositAndBorrowSameAsset); @@ -378,13 +425,20 @@ module suilend::obligation { obligation.unweighted_borrowed_value_usd, borrow_market_value ); + + let borrow_weight = if (is_emode) { + decimal::from(1) + } else { + borrow_weight(config(reserve)) + }; + obligation.weighted_borrowed_value_usd = add( obligation.weighted_borrowed_value_usd, - mul(borrow_market_value, borrow_weight(config(reserve))) + mul(borrow_market_value, borrow_weight) ); obligation.weighted_borrowed_value_upper_bound_usd = add( obligation.weighted_borrowed_value_upper_bound_usd, - mul(borrow_market_value_upper_bound, borrow_weight(config(reserve))) + mul(borrow_market_value_upper_bound, borrow_weight) ); let user_reward_manager = vector::borrow_mut(&mut obligation.user_reward_managers, borrow.user_reward_manager_index); @@ -410,6 +464,8 @@ module suilend::obligation { clock: &Clock, max_repay_amount: Decimal, ): Decimal { + let is_emode = is_emode(obligation); + let borrow_index = find_borrow_index(obligation, reserve); assert!(borrow_index < vector::length(&obligation.borrows), EBorrowNotFound); let borrow = vector::borrow_mut(&mut obligation.borrows, borrow_index); @@ -423,6 +479,12 @@ module suilend::obligation { borrow.borrowed_amount = sub(borrow.borrowed_amount, repay_amount); + let borrow_weight = if (is_emode) { + decimal::from(1) + } else { + borrow_weight(config(reserve)) + }; + // update other health values. note that we don't enforce price freshness here. this is purely // to make offchain accounting easier. any operation that requires price // freshness (withdraw, borrow, liquidate) will refresh the obligation right before. @@ -438,11 +500,11 @@ module suilend::obligation { ); obligation.weighted_borrowed_value_usd = saturating_sub( obligation.weighted_borrowed_value_usd, - mul(repay_value, borrow_weight(config(reserve))) + mul(repay_value, borrow_weight) ); obligation.weighted_borrowed_value_upper_bound_usd = saturating_sub( obligation.weighted_borrowed_value_upper_bound_usd, - mul(repay_value_upper_bound, borrow_weight(config(reserve))) + mul(repay_value_upper_bound, borrow_weight) ); } else { @@ -457,11 +519,11 @@ module suilend::obligation { ); obligation.weighted_borrowed_value_usd = add( obligation.weighted_borrowed_value_usd, - mul(additional_borrow_value, borrow_weight(config(reserve))) + mul(additional_borrow_value, borrow_weight) ); obligation.weighted_borrowed_value_upper_bound_usd = add( obligation.weighted_borrowed_value_upper_bound_usd, - mul(additional_borrow_value_upper_bound, borrow_weight(config(reserve))) + mul(additional_borrow_value_upper_bound, borrow_weight) ); }; @@ -890,8 +952,17 @@ module suilend::obligation { clock: &Clock, ctoken_amount: u64, ) { + let is_emode = is_emode(obligation); + let deposit_index = find_deposit_index(obligation, reserve); assert!(deposit_index < vector::length(&obligation.deposits), EDepositNotFound); + + let (open_ltv, close_ltv) = get_ltvs( + obligation, + reserve, + is_emode, + ); + let deposit = vector::borrow_mut(&mut obligation.deposits, deposit_index); let withdraw_market_value = reserve::ctoken_market_value(reserve, ctoken_amount); @@ -906,14 +977,14 @@ module suilend::obligation { mul( // need to use lower bound to keep calculation consistent reserve::ctoken_market_value_lower_bound(reserve, ctoken_amount), - open_ltv(config(reserve)) + open_ltv ) ); obligation.unhealthy_borrow_value_usd = sub( obligation.unhealthy_borrow_value_usd, mul( withdraw_market_value, - close_ltv(config(reserve)) + close_ltv ) ); @@ -1120,6 +1191,39 @@ module suilend::obligation { (length - 1, vector::borrow_mut(&mut obligation.user_reward_managers, length - 1)) } + fun get_ltvs

( + obligation: &Obligation

, + deposit_reserve: &Reserve

, + is_emode: bool, + ): (Decimal, Decimal) { + if (is_emode) { + let borrow_reserve_array_index = df::borrow(&obligation.id, EModeFlag {}); + + let emode_data = reserve_config::get_emode_ltvs( + config(deposit_reserve), + borrow_reserve_array_index + ); + + (open_ltv_emode(emode_data), close_ltv_emode(emode_data)) + } else { + (open_ltv(config(deposit_reserve)), close_ltv(config(deposit_reserve))) + } + } + + fun is_emode

(obligation: &Obligation

): bool { + df::exists_(&obligation.id, EModeFlag {}) + } + + fun emode_borrow_reserve_array_index

(obligation: &Obligation

): u64 { + let emode_idx: &EModeReserveIndices = df::borrow(&obligation.id, EModeFlag {}); + emode_idx.borrow_reserve_array_index + } + + fun emode_deposit_reserve_array_index

(obligation: &Obligation

): u64 { + let emode_idx: &EModeReserveIndices = df::borrow(&obligation.id, EModeFlag {}); + emode_idx.deposit_reserve_array_index + } + // === Test Functions === #[test_only] struct TEST_MARKET {} diff --git a/contracts/sources/reserve_config.move b/contracts/sources/reserve_config.move index 3d85ee1..0ad5755 100644 --- a/contracts/sources/reserve_config.move +++ b/contracts/sources/reserve_config.move @@ -63,14 +63,8 @@ module suilend::reserve_config { fields: Bag } - // struct EmodeConfig has store { - // // Corresponding to the deposited coin type - // reserve_array_indices: vector, - // emode_pairs: vector - // } - struct EModeData has store, copy, drop { - // Corresponding to the correlated pair + // Corresponding borrow reserve index reserve_array_index: u64, open_ltv_pct: u8, close_ltv_pct: u8, @@ -471,6 +465,14 @@ module suilend::reserve_config { }; } + public(friend) fun check_emode_validity( + reserve_config: &ReserveConfig, + reserve_array_index: &u64, + ): bool { + let emode_config = get_emode_config(reserve_config); + vec_map::contains(emode_config, reserve_array_index) + } + public(friend) fun get_emode_config( reserve_config: &ReserveConfig, ): &VecMap { From 77bd53f91aa38e46c2ed5359dcf0e0f754190e21 Mon Sep 17 00:00:00 2001 From: 0xxgen1 <0xxgen@solend.fi> Date: Thu, 3 Oct 2024 19:02:32 +0100 Subject: [PATCH 6/9] Reserve config tests --- contracts/sources/reserve_config.move | 109 +++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/contracts/sources/reserve_config.move b/contracts/sources/reserve_config.move index 0ad5755..8082772 100644 --- a/contracts/sources/reserve_config.move +++ b/contracts/sources/reserve_config.move @@ -494,10 +494,10 @@ module suilend::reserve_config { public(friend) fun close_ltv_emode( emode_data: &EModeData, ): Decimal { - decimal::from_percent(emode_data.open_ltv_pct) + decimal::from_percent(emode_data.close_ltv_pct) } - public(friend) fun get_emode_ltvs( + public(friend) fun get_emode_data( reserve_config: &ReserveConfig, reserve_array_index: &u64, ): &EModeData { @@ -690,4 +690,109 @@ module suilend::reserve_config { config } + #[test] + fun test_emode_reserve_config() { + use sui::test_utils::assert_eq; + + let owner = @0x26; + let scenario = test_scenario::begin(owner); + + let utils = vector::empty(); + vector::push_back(&mut utils, 0); + vector::push_back(&mut utils, 100); + + let aprs = vector::empty(); + vector::push_back(&mut aprs, 0); + vector::push_back(&mut aprs, 100); + + let config = create_reserve_config( + 10, + 10, + 10, + 10_000, + 1, + 1, + 5, + 5, + 100000, + 100000, + 10, + 2000, + 30, + utils, + aprs, + false, + 0, + 0, + test_scenario::ctx(&mut scenario) + ); + + set_emode_for_pair( + &mut config, + 1, + 80, + 60, + ); + + check_emode_validity(&config, &1); + + assert!(has_emode_config(&config), 0); + let emode_data = get_emode_data(&config, &1); + assert_eq(open_ltv_emode(emode_data), decimal::from_percent(80)); + assert_eq(close_ltv_emode(emode_data), decimal::from_percent(60)); + + destroy(config); + test_scenario::end(scenario); + } + + #[test] + fun test_fail_emode_validity() { + use sui::test_utils::assert_eq; + + let owner = @0x26; + let scenario = test_scenario::begin(owner); + + let utils = vector::empty(); + vector::push_back(&mut utils, 0); + vector::push_back(&mut utils, 100); + + let aprs = vector::empty(); + vector::push_back(&mut aprs, 0); + vector::push_back(&mut aprs, 100); + + let config = create_reserve_config( + 10, + 10, + 10, + 10_000, + 1, + 1, + 5, + 5, + 100000, + 100000, + 10, + 2000, + 30, + utils, + aprs, + false, + 0, + 0, + test_scenario::ctx(&mut scenario) + ); + + set_emode_for_pair( + &mut config, + 1, + 80, + 60, + ); + + assert_eq(check_emode_validity(&config, &2), false); + + + destroy(config); + test_scenario::end(scenario); + } } From 5cd2ea05db66849883d9169debef371c56af75bf Mon Sep 17 00:00:00 2001 From: 0xxgen1 <0xxgen@solend.fi> Date: Thu, 3 Oct 2024 20:13:48 +0100 Subject: [PATCH 7/9] Obligation tests --- contracts/sources/lending_market.move | 6 +- contracts/sources/obligation.move | 401 +++++++++++++++++++++++++- contracts/sources/reserve_config.move | 8 +- 3 files changed, 398 insertions(+), 17 deletions(-) diff --git a/contracts/sources/lending_market.move b/contracts/sources/lending_market.move index c06c67c..c23ee93 100644 --- a/contracts/sources/lending_market.move +++ b/contracts/sources/lending_market.move @@ -381,7 +381,7 @@ module suilend::lending_market { } /// Set emode for obligation - T is the deposit coin type - public fun set_emode( + public fun set_emode

( lending_market: &mut LendingMarket

, deposit_reserve_array_index: u64, borrow_reserve_array_index: u64, @@ -395,12 +395,12 @@ module suilend::lending_market { ); let deposit_reserve = vector::borrow(&lending_market.reserves, deposit_reserve_array_index); - assert!(reserve::coin_type(deposit_reserve) == type_name::get(), EWrongType); + let borrow_reserve = vector::borrow(&lending_market.reserves, borrow_reserve_array_index); obligation::set_emode( obligation, deposit_reserve, - borrow_reserve_array_index + borrow_reserve, ); } diff --git a/contracts/sources/obligation.move b/contracts/sources/obligation.move index 3d7e4c1..1c1c37d 100644 --- a/contracts/sources/obligation.move +++ b/contracts/sources/obligation.move @@ -42,8 +42,12 @@ module suilend::obligation { const EObligationIsNotForgivable: u64 = 7; const ECannotDepositAndBorrowSameAsset: u64 = 8; const EEmodeInvalidForPairProvided: u64 = 9; - const EInvalidEModeDeposit: u64 = 10; - const EInvalidEModeBorrow: u64 = 11; + const EEModeNotValidWithCrossMargin: u64 = 10; + const EEModeObligationBorrowNotValid: u64 = 11; + const EEModeObligationDepositNotValid: u64 = 12; + const EEmodeCrossMarginNotAllowed: u64 = 13; + const EInvalidEModeDeposit: u64 = 14; + const EInvalidEModeBorrow: u64 = 15; // === Constants === const CLOSE_FACTOR_PCT: u8 = 20; @@ -183,16 +187,37 @@ module suilend::obligation { public(friend) fun set_emode

( obligation: &mut Obligation

, deposit_reserve: &Reserve

, - borrow_reserve_array_index: u64, + borrow_reserve: &Reserve

, ) { + assert!(vector::length(&obligation.borrows) <= 1, EEModeNotValidWithCrossMargin); + assert!(vector::length(&obligation.deposits) <= 1, EEModeNotValidWithCrossMargin); + let deposit_reserve_array_index = reserve::array_index(deposit_reserve); + let borrow_reserve_array_index = reserve::array_index(borrow_reserve); let is_valid_emode = reserve_config::check_emode_validity( config(deposit_reserve), &borrow_reserve_array_index ); - assert!(is_valid_emode, EEmodeInvalidForPairProvided); + // The obligation must ONLY have one deposit and one borrow, and + // they both must match the emod ones + if (vector::length(&obligation.borrows) == 1) { + let borrow = vector::borrow(&obligation.borrows, 0); + assert!( + borrow.reserve_array_index == borrow_reserve_array_index, + EEModeObligationDepositNotValid + ); + }; + + if (vector::length(&obligation.deposits) == 1) { + let deposit = vector::borrow(&obligation.deposits, 0); + assert!( + deposit.reserve_array_index == deposit_reserve_array_index, + EEModeObligationBorrowNotValid + ); + }; + df::add(&mut obligation.id, EModeFlag {}, EModeReserveIndices { deposit_reserve_array_index, borrow_reserve_array_index, @@ -261,7 +286,6 @@ module suilend::obligation { obligation.allowed_borrow_value_usd = allowed_borrow_value_usd; obligation.unhealthy_borrow_value_usd = unhealthy_borrow_value_usd; - let i = 0; let unweighted_borrowed_value_usd = decimal::from(0); let weighted_borrowed_value_usd = decimal::from(0); @@ -337,7 +361,7 @@ module suilend::obligation { let is_emode = is_emode(obligation); if (is_emode) { - assert!(vector::length(&obligation.deposits) == 1, ETooManyDeposits); + assert!(vector::length(&obligation.deposits) == 1, EEmodeCrossMarginNotAllowed); let target_reserve_index = emode_deposit_reserve_array_index(obligation); let deposit = vector::borrow(&obligation.deposits, deposit_index); @@ -402,7 +426,7 @@ module suilend::obligation { let is_emode = is_emode(obligation); if (is_emode) { - assert!(vector::length(&obligation.borrows) == 1, ETooManyBorrows); + assert!(vector::length(&obligation.borrows) == 1, EEmodeCrossMarginNotAllowed); let target_reserve_index = emode_borrow_reserve_array_index(obligation); let borrow = vector::borrow(&obligation.borrows, borrow_index); assert!(borrow.reserve_array_index == target_reserve_index, EInvalidEModeBorrow); @@ -1197,11 +1221,11 @@ module suilend::obligation { is_emode: bool, ): (Decimal, Decimal) { if (is_emode) { - let borrow_reserve_array_index = df::borrow(&obligation.id, EModeFlag {}); + let indices: &EModeReserveIndices = df::borrow(&obligation.id, EModeFlag {}); - let emode_data = reserve_config::get_emode_ltvs( + let emode_data = reserve_config::get_emode_data( config(deposit_reserve), - borrow_reserve_array_index + &indices.borrow_reserve_array_index ); (open_ltv_emode(emode_data), close_ltv_emode(emode_data)) @@ -3126,4 +3150,361 @@ module suilend::obligation { sui::test_utils::destroy(obligation); test_scenario::end(scenario); } + + #[test] + public fun test_emode_deposit_borrow() { + use sui::test_scenario::{Self}; + + let owner = @0x26; + let scenario = test_scenario::begin(owner); + let lending_market_id = object::new(test_scenario::ctx(&mut scenario)); + + let usdc_reserve = usdc_reserve(&mut scenario); + let sui_reserve = sui_reserve(&mut scenario); + + let obligation = create_obligation(object::uid_to_inner(&lending_market_id), test_scenario::ctx(&mut scenario)); + let clock = clock::create_for_testing(test_scenario::ctx(&mut scenario)); + + reserve::update_price_for_testing( + &mut usdc_reserve, + &clock, + decimal::from(1), + decimal::from(2) + ); + reserve::update_price_for_testing( + &mut sui_reserve, + &clock, + decimal::from(10), + decimal::from(5) + ); + + reserve::set_emode_for_pair( + &mut sui_reserve, + reserve::array_index(&usdc_reserve), + 40, // 20 + 80, // 50 + ); + + set_emode( + &mut obligation, + &sui_reserve, // deposit + &usdc_reserve, // borrow + ); + + deposit(&mut obligation, &mut sui_reserve, &clock, 100 * 1_000_000_000); + borrow(&mut obligation, &mut usdc_reserve, &clock, 12_500_000); + borrow(&mut obligation, &mut usdc_reserve, &clock, 12_500_000); + + assert!(vector::length(&obligation.deposits) == 1, 0); + + let sui_deposit = vector::borrow(&obligation.deposits, 0); + assert!(sui_deposit.deposited_ctoken_amount == 100 * 1_000_000_000, 3); + assert!(sui_deposit.market_value == decimal::from(1000), 4); + + let user_reward_manager = vector::borrow(&obligation.user_reward_managers, sui_deposit.user_reward_manager_index); + assert!(liquidity_mining::shares(user_reward_manager) == 100 * 1_000_000_000, 3); + + assert!(vector::length(&obligation.borrows) == 1, 0); + + let usdc_borrow = vector::borrow(&obligation.borrows, 0); + assert!(usdc_borrow.borrowed_amount == decimal::from(25 * 1_000_000), 1); + assert!(usdc_borrow.cumulative_borrow_rate == decimal::from(2), 2); + assert!(usdc_borrow.market_value == decimal::from(25), 3); + + let user_reward_manager = vector::borrow(&obligation.user_reward_managers, usdc_borrow.user_reward_manager_index); + assert!(liquidity_mining::shares(user_reward_manager) == 25 * 1_000_000 / 2, 4); + + // Values unchanged due to emode + assert!(obligation.deposited_value_usd == decimal::from(1000), 0); + assert!(obligation.unweighted_borrowed_value_usd == decimal::from(25), 3); + + // Values changed due to emode + assert!(obligation.allowed_borrow_value_usd == decimal::from(200), 1); + assert!(obligation.unhealthy_borrow_value_usd == decimal::from(800), 2); + assert!(obligation.weighted_borrowed_value_usd == decimal::from(25), 4); + assert!(obligation.weighted_borrowed_value_upper_bound_usd == decimal::from(50), 4); + + sui::test_utils::destroy(lending_market_id); + sui::test_utils::destroy(usdc_reserve); + sui::test_utils::destroy(sui_reserve); + sui::test_utils::destroy(obligation); + clock::destroy_for_testing(clock); + test_scenario::end(scenario); + } + + #[test] + public fun test_emode_multiple_borrows_happy() { + use sui::test_scenario::{Self}; + + let owner = @0x26; + let scenario = test_scenario::begin(owner); + let lending_market_id = object::new(test_scenario::ctx(&mut scenario)); + + let usdc_reserve = usdc_reserve(&mut scenario); + let sui_reserve = sui_reserve(&mut scenario); + + let obligation = create_obligation(object::uid_to_inner(&lending_market_id), test_scenario::ctx(&mut scenario)); + let clock = clock::create_for_testing(test_scenario::ctx(&mut scenario)); + + reserve::update_price_for_testing( + &mut usdc_reserve, + &clock, + decimal::from(1), + decimal::from(2) + ); + reserve::update_price_for_testing( + &mut sui_reserve, + &clock, + decimal::from(10), + decimal::from(5) + ); + + reserve::set_emode_for_pair( + &mut sui_reserve, + reserve::array_index(&usdc_reserve), + 40, + 60, + ); + + set_emode( + &mut obligation, + &sui_reserve, // deposit + &usdc_reserve, // borrow + ); + + deposit(&mut obligation, &mut sui_reserve, &clock, 100 * 1_000_000_000); + borrow(&mut obligation, &mut usdc_reserve, &clock, 12_500_000); + borrow(&mut obligation, &mut usdc_reserve, &clock, 12_500_000); + + sui::test_utils::destroy(lending_market_id); + sui::test_utils::destroy(usdc_reserve); + sui::test_utils::destroy(sui_reserve); + sui::test_utils::destroy(obligation); + clock::destroy_for_testing(clock); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = EEmodeCrossMarginNotAllowed)] + public fun test_emode_multiple_borrows_fail() { + use sui::test_scenario::{Self}; + + let owner = @0x26; + let scenario = test_scenario::begin(owner); + let lending_market_id = object::new(test_scenario::ctx(&mut scenario)); + + let usdc_reserve = usdc_reserve(&mut scenario); + let usdt_reserve = usdt_reserve(&mut scenario); + let sui_reserve = sui_reserve(&mut scenario); + + let obligation = create_obligation(object::uid_to_inner(&lending_market_id), test_scenario::ctx(&mut scenario)); + let clock = clock::create_for_testing(test_scenario::ctx(&mut scenario)); + + reserve::update_price_for_testing( + &mut usdc_reserve, + &clock, + decimal::from(1), + decimal::from(2) + ); + reserve::update_price_for_testing( + &mut sui_reserve, + &clock, + decimal::from(10), + decimal::from(5) + ); + reserve::update_price_for_testing( + &mut usdt_reserve, + &clock, + decimal::from(1), + decimal::from(2) + ); + + reserve::set_emode_for_pair( + &mut sui_reserve, + reserve::array_index(&usdc_reserve), + 40, + 60, + ); + + set_emode( + &mut obligation, + &sui_reserve, // deposit + &usdc_reserve, // borrow + ); + + deposit(&mut obligation, &mut sui_reserve, &clock, 100 * 1_000_000_000); + borrow(&mut obligation, &mut usdc_reserve, &clock, 12_500_000); + borrow(&mut obligation, &mut usdt_reserve, &clock, 12_500_000); + + sui::test_utils::destroy(lending_market_id); + sui::test_utils::destroy(usdc_reserve); + sui::test_utils::destroy(usdt_reserve); + sui::test_utils::destroy(sui_reserve); + sui::test_utils::destroy(obligation); + clock::destroy_for_testing(clock); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = EInvalidEModeBorrow)] + public fun test_emode_invalid_borrow_fail() { + use sui::test_scenario::{Self}; + + let owner = @0x26; + let scenario = test_scenario::begin(owner); + let lending_market_id = object::new(test_scenario::ctx(&mut scenario)); + + let usdc_reserve = usdc_reserve(&mut scenario); + let usdt_reserve = usdt_reserve(&mut scenario); + let sui_reserve = sui_reserve(&mut scenario); + + let obligation = create_obligation(object::uid_to_inner(&lending_market_id), test_scenario::ctx(&mut scenario)); + let clock = clock::create_for_testing(test_scenario::ctx(&mut scenario)); + + reserve::update_price_for_testing( + &mut usdc_reserve, + &clock, + decimal::from(1), + decimal::from(2) + ); + reserve::update_price_for_testing( + &mut sui_reserve, + &clock, + decimal::from(10), + decimal::from(5) + ); + reserve::update_price_for_testing( + &mut usdt_reserve, + &clock, + decimal::from(1), + decimal::from(2) + ); + + reserve::set_emode_for_pair( + &mut sui_reserve, + reserve::array_index(&usdc_reserve), + 40, + 60, + ); + + set_emode( + &mut obligation, + &sui_reserve, // deposit + &usdc_reserve, // borrow + ); + + deposit(&mut obligation, &mut sui_reserve, &clock, 100 * 1_000_000_000); + borrow(&mut obligation, &mut usdt_reserve, &clock, 12_500_000); + + sui::test_utils::destroy(lending_market_id); + sui::test_utils::destroy(usdc_reserve); + sui::test_utils::destroy(usdt_reserve); + sui::test_utils::destroy(sui_reserve); + sui::test_utils::destroy(obligation); + clock::destroy_for_testing(clock); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = EEmodeCrossMarginNotAllowed)] + public fun test_emode_multiple_deposits_fail() { + use sui::test_scenario::{Self}; + + let owner = @0x26; + let scenario = test_scenario::begin(owner); + let lending_market_id = object::new(test_scenario::ctx(&mut scenario)); + + let usdc_reserve = usdc_reserve(&mut scenario); + let sui_reserve = sui_reserve(&mut scenario); + + let obligation = create_obligation(object::uid_to_inner(&lending_market_id), test_scenario::ctx(&mut scenario)); + let clock = clock::create_for_testing(test_scenario::ctx(&mut scenario)); + + reserve::update_price_for_testing( + &mut usdc_reserve, + &clock, + decimal::from(1), + decimal::from(2) + ); + reserve::update_price_for_testing( + &mut sui_reserve, + &clock, + decimal::from(10), + decimal::from(5) + ); + + reserve::set_emode_for_pair( + &mut sui_reserve, + reserve::array_index(&usdc_reserve), + 40, + 60, + ); + + set_emode( + &mut obligation, + &sui_reserve, // deposit + &usdc_reserve, // borrow + ); + + deposit(&mut obligation, &mut sui_reserve, &clock, 100 * 1_000_000_000); + deposit(&mut obligation, &mut usdc_reserve, &clock, 12_500_000); + + sui::test_utils::destroy(lending_market_id); + sui::test_utils::destroy(usdc_reserve); + sui::test_utils::destroy(sui_reserve); + sui::test_utils::destroy(obligation); + clock::destroy_for_testing(clock); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = EInvalidEModeDeposit)] + public fun test_emode_invalid_deposit_fail() { + use sui::test_scenario::{Self}; + + let owner = @0x26; + let scenario = test_scenario::begin(owner); + let lending_market_id = object::new(test_scenario::ctx(&mut scenario)); + + let usdc_reserve = usdc_reserve(&mut scenario); + let sui_reserve = sui_reserve(&mut scenario); + + let obligation = create_obligation(object::uid_to_inner(&lending_market_id), test_scenario::ctx(&mut scenario)); + let clock = clock::create_for_testing(test_scenario::ctx(&mut scenario)); + + reserve::update_price_for_testing( + &mut usdc_reserve, + &clock, + decimal::from(1), + decimal::from(2) + ); + reserve::update_price_for_testing( + &mut sui_reserve, + &clock, + decimal::from(10), + decimal::from(5) + ); + + reserve::set_emode_for_pair( + &mut sui_reserve, + reserve::array_index(&usdc_reserve), + 40, + 60, + ); + + set_emode( + &mut obligation, + &sui_reserve, // deposit + &usdc_reserve, // borrow + ); + + deposit(&mut obligation, &mut usdc_reserve, &clock, 12_500_000); + + sui::test_utils::destroy(lending_market_id); + sui::test_utils::destroy(usdc_reserve); + sui::test_utils::destroy(sui_reserve); + sui::test_utils::destroy(obligation); + clock::destroy_for_testing(clock); + test_scenario::end(scenario); + } } diff --git a/contracts/sources/reserve_config.move b/contracts/sources/reserve_config.move index 8082772..8143689 100644 --- a/contracts/sources/reserve_config.move +++ b/contracts/sources/reserve_config.move @@ -730,16 +730,16 @@ module suilend::reserve_config { set_emode_for_pair( &mut config, 1, - 80, 60, + 80, ); check_emode_validity(&config, &1); assert!(has_emode_config(&config), 0); let emode_data = get_emode_data(&config, &1); - assert_eq(open_ltv_emode(emode_data), decimal::from_percent(80)); - assert_eq(close_ltv_emode(emode_data), decimal::from_percent(60)); + assert_eq(open_ltv_emode(emode_data), decimal::from_percent(60)); + assert_eq(close_ltv_emode(emode_data), decimal::from_percent(80)); destroy(config); test_scenario::end(scenario); @@ -785,8 +785,8 @@ module suilend::reserve_config { set_emode_for_pair( &mut config, 1, - 80, 60, + 80, ); assert_eq(check_emode_validity(&config, &2), false); From 96927262a5143cb8334f0b62812f23fd170a5269 Mon Sep 17 00:00:00 2001 From: 0xxgen1 <0xxgen@solend.fi> Date: Fri, 4 Oct 2024 13:07:14 +0100 Subject: [PATCH 8/9] - Addressed review items - Increase test coverage --- contracts/sources/obligation.move | 565 +++++++++++++++++++++++--- contracts/sources/reserve_config.move | 17 +- 2 files changed, 515 insertions(+), 67 deletions(-) diff --git a/contracts/sources/obligation.move b/contracts/sources/obligation.move index 1c1c37d..3542f34 100644 --- a/contracts/sources/obligation.move +++ b/contracts/sources/obligation.move @@ -43,11 +43,11 @@ module suilend::obligation { const ECannotDepositAndBorrowSameAsset: u64 = 8; const EEmodeInvalidForPairProvided: u64 = 9; const EEModeNotValidWithCrossMargin: u64 = 10; - const EEModeObligationBorrowNotValid: u64 = 11; - const EEModeObligationDepositNotValid: u64 = 12; - const EEmodeCrossMarginNotAllowed: u64 = 13; - const EInvalidEModeDeposit: u64 = 14; - const EInvalidEModeBorrow: u64 = 15; + const EEModeObligationBorrowMismatch: u64 = 11; + const EEModeObligationDepositMismatch: u64 = 12; + const EInvalidEModeDeposit: u64 = 13; + const EInvalidEModeBorrow: u64 = 14; + const EEModeAlreadySet: u64 = 15; // === Constants === const CLOSE_FACTOR_PCT: u8 = 20; @@ -189,6 +189,7 @@ module suilend::obligation { deposit_reserve: &Reserve

, borrow_reserve: &Reserve

, ) { + assert!(!is_emode(obligation), EEModeAlreadySet); assert!(vector::length(&obligation.borrows) <= 1, EEModeNotValidWithCrossMargin); assert!(vector::length(&obligation.deposits) <= 1, EEModeNotValidWithCrossMargin); @@ -206,7 +207,7 @@ module suilend::obligation { let borrow = vector::borrow(&obligation.borrows, 0); assert!( borrow.reserve_array_index == borrow_reserve_array_index, - EEModeObligationDepositNotValid + EEModeObligationBorrowMismatch ); }; @@ -214,7 +215,7 @@ module suilend::obligation { let deposit = vector::borrow(&obligation.deposits, 0); assert!( deposit.reserve_array_index == deposit_reserve_array_index, - EEModeObligationBorrowNotValid + EEModeObligationDepositMismatch ); }; @@ -315,25 +316,20 @@ module suilend::obligation { } else { borrow_weight(config(borrow_reserve)) }; - - let borrow_weighted_value_usd = mul( - market_value, - borrow_weight - ); weighted_borrowed_value_usd = add( weighted_borrowed_value_usd, - borrow_weighted_value_usd - ); - - let borrow_weighted_value_upper_bound_usd = mul( - market_value_upper_bound, - borrow_weight + mul( + market_value, + borrow_weight + ) ); - weighted_borrowed_value_upper_bound_usd = add( weighted_borrowed_value_upper_bound_usd, - borrow_weighted_value_upper_bound_usd, + mul( + market_value_upper_bound, + borrow_weight + ) ); if (isolated(config(borrow_reserve))) { @@ -358,21 +354,12 @@ module suilend::obligation { ctoken_amount: u64, ) { let deposit_index = find_or_add_deposit(obligation, reserve, clock); - let is_emode = is_emode(obligation); - - if (is_emode) { - assert!(vector::length(&obligation.deposits) == 1, EEmodeCrossMarginNotAllowed); - - let target_reserve_index = emode_deposit_reserve_array_index(obligation); - let deposit = vector::borrow(&obligation.deposits, deposit_index); - assert!(deposit.reserve_array_index == target_reserve_index, EInvalidEModeDeposit); - } else { - assert!(vector::length(&obligation.deposits) <= MAX_DEPOSITS, ETooManyDeposits); - }; + assert!(vector::length(&obligation.deposits) <= MAX_DEPOSITS, ETooManyDeposits); let borrow_index = find_borrow_index(obligation, reserve); assert!(borrow_index == vector::length(&obligation.borrows), ECannotDepositAndBorrowSameAsset); + let is_emode = is_emode(obligation); let (open_ltv, close_ltv) = get_ltvs( obligation, reserve, @@ -412,6 +399,14 @@ module suilend::obligation { deposit.deposited_ctoken_amount, clock ); + + if (is_emode) { + assert!(vector::length(&obligation.deposits) == 1, EIsolatedAssetViolation); + let target_reserve_index = emode_deposit_reserve_array_index(obligation); + let deposit = vector::borrow(&obligation.deposits, deposit_index); + assert!(deposit.reserve_array_index == target_reserve_index, EInvalidEModeDeposit); + }; + log_obligation_data(obligation); } @@ -423,15 +418,13 @@ module suilend::obligation { amount: u64, ) { let borrow_index = find_or_add_borrow(obligation, reserve, clock); + assert!(vector::length(&obligation.borrows) <= MAX_BORROWS, ETooManyBorrows); + let is_emode = is_emode(obligation); - - if (is_emode) { - assert!(vector::length(&obligation.borrows) == 1, EEmodeCrossMarginNotAllowed); - let target_reserve_index = emode_borrow_reserve_array_index(obligation); - let borrow = vector::borrow(&obligation.borrows, borrow_index); - assert!(borrow.reserve_array_index == target_reserve_index, EInvalidEModeBorrow); + let borrow_weight = if (is_emode) { + decimal::from(1) } else { - assert!(vector::length(&obligation.borrows) <= MAX_BORROWS, ETooManyBorrows); + borrow_weight(config(reserve)) }; let deposit_index = find_deposit_index(obligation, reserve); @@ -449,13 +442,6 @@ module suilend::obligation { obligation.unweighted_borrowed_value_usd, borrow_market_value ); - - let borrow_weight = if (is_emode) { - decimal::from(1) - } else { - borrow_weight(config(reserve)) - }; - obligation.weighted_borrowed_value_usd = add( obligation.weighted_borrowed_value_usd, mul(borrow_market_value, borrow_weight) @@ -475,9 +461,16 @@ module suilend::obligation { assert!(is_healthy(obligation), EObligationIsNotHealthy); - if (isolated(config(reserve)) || obligation.borrowing_isolated_asset) { + if (isolated(config(reserve)) || obligation.borrowing_isolated_asset || is_emode) { assert!(vector::length(&obligation.borrows) == 1, EIsolatedAssetViolation); }; + + if (is_emode) { + let target_reserve_index = emode_borrow_reserve_array_index(obligation); + let borrow = vector::borrow(&obligation.borrows, borrow_index); + assert!(borrow.reserve_array_index == target_reserve_index, EInvalidEModeBorrow); + }; + log_obligation_data(obligation); } @@ -489,6 +482,11 @@ module suilend::obligation { max_repay_amount: Decimal, ): Decimal { let is_emode = is_emode(obligation); + let borrow_weight = if (is_emode) { + decimal::from(1) + } else { + borrow_weight(config(reserve)) + }; let borrow_index = find_borrow_index(obligation, reserve); assert!(borrow_index < vector::length(&obligation.borrows), EBorrowNotFound); @@ -503,12 +501,6 @@ module suilend::obligation { borrow.borrowed_amount = sub(borrow.borrowed_amount, repay_amount); - let borrow_weight = if (is_emode) { - decimal::from(1) - } else { - borrow_weight(config(reserve)) - }; - // update other health values. note that we don't enforce price freshness here. this is purely // to make offchain accounting easier. any operation that requires price // freshness (withdraw, borrow, liquidate) will refresh the obligation right before. @@ -976,11 +968,10 @@ module suilend::obligation { clock: &Clock, ctoken_amount: u64, ) { - let is_emode = is_emode(obligation); - let deposit_index = find_deposit_index(obligation, reserve); assert!(deposit_index < vector::length(&obligation.deposits), EDepositNotFound); + let is_emode = is_emode(obligation); let (open_ltv, close_ltv) = get_ltvs( obligation, reserve, @@ -1223,7 +1214,7 @@ module suilend::obligation { if (is_emode) { let indices: &EModeReserveIndices = df::borrow(&obligation.id, EModeFlag {}); - let emode_data = reserve_config::get_emode_data( + let emode_data = reserve_config::get_emode_data_checked( config(deposit_reserve), &indices.borrow_reserve_array_index ); @@ -3285,7 +3276,7 @@ module suilend::obligation { } #[test] - #[expected_failure(abort_code = EEmodeCrossMarginNotAllowed)] + #[expected_failure(abort_code = EIsolatedAssetViolation)] public fun test_emode_multiple_borrows_fail() { use sui::test_scenario::{Self}; @@ -3406,7 +3397,7 @@ module suilend::obligation { } #[test] - #[expected_failure(abort_code = EEmodeCrossMarginNotAllowed)] + #[expected_failure(abort_code = EIsolatedAssetViolation)] public fun test_emode_multiple_deposits_fail() { use sui::test_scenario::{Self}; @@ -3414,6 +3405,7 @@ module suilend::obligation { let scenario = test_scenario::begin(owner); let lending_market_id = object::new(test_scenario::ctx(&mut scenario)); + let usdt_reserve = usdt_reserve(&mut scenario); let usdc_reserve = usdc_reserve(&mut scenario); let sui_reserve = sui_reserve(&mut scenario); @@ -3440,6 +3432,13 @@ module suilend::obligation { 60, ); + reserve::set_emode_for_pair( + &mut usdt_reserve, + reserve::array_index(&usdc_reserve), + 40, + 60, + ); + set_emode( &mut obligation, &sui_reserve, // deposit @@ -3447,10 +3446,11 @@ module suilend::obligation { ); deposit(&mut obligation, &mut sui_reserve, &clock, 100 * 1_000_000_000); - deposit(&mut obligation, &mut usdc_reserve, &clock, 12_500_000); + deposit(&mut obligation, &mut usdt_reserve, &clock, 12_500_000); sui::test_utils::destroy(lending_market_id); sui::test_utils::destroy(usdc_reserve); + sui::test_utils::destroy(usdt_reserve); sui::test_utils::destroy(sui_reserve); sui::test_utils::destroy(obligation); clock::destroy_for_testing(clock); @@ -3466,6 +3466,7 @@ module suilend::obligation { let scenario = test_scenario::begin(owner); let lending_market_id = object::new(test_scenario::ctx(&mut scenario)); + let usdt_reserve = usdt_reserve(&mut scenario); let usdc_reserve = usdc_reserve(&mut scenario); let sui_reserve = sui_reserve(&mut scenario); @@ -3491,6 +3492,58 @@ module suilend::obligation { 40, 60, ); + + reserve::set_emode_for_pair( + &mut usdt_reserve, + reserve::array_index(&usdc_reserve), + 40, + 60, + ); + + set_emode( + &mut obligation, + &sui_reserve, // deposit + &usdc_reserve, // borrow + ); + + deposit(&mut obligation, &mut usdt_reserve, &clock, 12_500_000); + + sui::test_utils::destroy(lending_market_id); + sui::test_utils::destroy(usdc_reserve); + sui::test_utils::destroy(usdt_reserve); + sui::test_utils::destroy(sui_reserve); + sui::test_utils::destroy(obligation); + clock::destroy_for_testing(clock); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = reserve_config::ENoEModeConfigForDepositReserve)] + public fun test_set_emode_invalid_deposit_reserve() { + use sui::test_scenario::{Self}; + + let owner = @0x26; + let scenario = test_scenario::begin(owner); + let lending_market_id = object::new(test_scenario::ctx(&mut scenario)); + + let usdc_reserve = usdc_reserve(&mut scenario); + let sui_reserve = sui_reserve(&mut scenario); + + let obligation = create_obligation(object::uid_to_inner(&lending_market_id), test_scenario::ctx(&mut scenario)); + let clock = clock::create_for_testing(test_scenario::ctx(&mut scenario)); + + reserve::update_price_for_testing( + &mut usdc_reserve, + &clock, + decimal::from(1), + decimal::from(2) + ); + reserve::update_price_for_testing( + &mut sui_reserve, + &clock, + decimal::from(10), + decimal::from(5) + ); set_emode( &mut obligation, @@ -3498,13 +3551,405 @@ module suilend::obligation { &usdc_reserve, // borrow ); - deposit(&mut obligation, &mut usdc_reserve, &clock, 12_500_000); + sui::test_utils::destroy(lending_market_id); + sui::test_utils::destroy(usdc_reserve); + sui::test_utils::destroy(sui_reserve); + sui::test_utils::destroy(obligation); + clock::destroy_for_testing(clock); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = reserve_config::EBorrowReserveIsNotAnEModePair)] + public fun test_emode_invalid_borrow_reserver_fail() { + use sui::test_scenario::{Self}; + + let owner = @0x26; + let scenario = test_scenario::begin(owner); + let lending_market_id = object::new(test_scenario::ctx(&mut scenario)); + + let usdc_reserve = usdc_reserve(&mut scenario); + let usdt_reserve = usdt_reserve(&mut scenario); + let sui_reserve = sui_reserve(&mut scenario); + + let obligation = create_obligation(object::uid_to_inner(&lending_market_id), test_scenario::ctx(&mut scenario)); + let clock = clock::create_for_testing(test_scenario::ctx(&mut scenario)); + + reserve::update_price_for_testing( + &mut usdc_reserve, + &clock, + decimal::from(1), + decimal::from(2) + ); + reserve::update_price_for_testing( + &mut sui_reserve, + &clock, + decimal::from(10), + decimal::from(5) + ); + + reserve::set_emode_for_pair( + &mut usdt_reserve, + reserve::array_index(&usdc_reserve), + 40, + 60, + ); + + reserve::set_emode_for_pair( + &mut sui_reserve, + reserve::array_index(&usdt_reserve), + 40, + 60, + ); + + set_emode( + &mut obligation, + &sui_reserve, // deposit + &usdt_reserve, // borrow + ); + + deposit(&mut obligation, &mut usdt_reserve, &clock, 12_500_000); sui::test_utils::destroy(lending_market_id); sui::test_utils::destroy(usdc_reserve); + sui::test_utils::destroy(usdt_reserve); + sui::test_utils::destroy(sui_reserve); + sui::test_utils::destroy(obligation); + clock::destroy_for_testing(clock); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = EEModeAlreadySet)] + public fun test_emode_already_set() { + use sui::test_scenario::{Self}; + + let owner = @0x26; + let scenario = test_scenario::begin(owner); + let lending_market_id = object::new(test_scenario::ctx(&mut scenario)); + + let usdc_reserve = usdc_reserve(&mut scenario); + let sui_reserve = sui_reserve(&mut scenario); + + let obligation = create_obligation(object::uid_to_inner(&lending_market_id), test_scenario::ctx(&mut scenario)); + let clock = clock::create_for_testing(test_scenario::ctx(&mut scenario)); + + reserve::update_price_for_testing( + &mut usdc_reserve, + &clock, + decimal::from(1), + decimal::from(2) + ); + reserve::update_price_for_testing( + &mut sui_reserve, + &clock, + decimal::from(10), + decimal::from(5) + ); + + reserve::set_emode_for_pair( + &mut sui_reserve, + reserve::array_index(&usdc_reserve), + 40, + 60, + ); + + set_emode( + &mut obligation, + &sui_reserve, // deposit + &usdc_reserve, // borrow + ); + + set_emode( + &mut obligation, + &sui_reserve, // deposit + &usdc_reserve, // borrow + ); + + sui::test_utils::destroy(lending_market_id); + sui::test_utils::destroy(usdc_reserve); + sui::test_utils::destroy(sui_reserve); + sui::test_utils::destroy(obligation); + clock::destroy_for_testing(clock); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = EEmodeInvalidForPairProvided)] + public fun test_set_emode_invalid_borrow_reserve() { + use sui::test_scenario::{Self}; + + let owner = @0x26; + let scenario = test_scenario::begin(owner); + let lending_market_id = object::new(test_scenario::ctx(&mut scenario)); + + let usdt_reserve = usdt_reserve(&mut scenario); + let usdc_reserve = usdc_reserve(&mut scenario); + let sui_reserve = sui_reserve(&mut scenario); + + let obligation = create_obligation(object::uid_to_inner(&lending_market_id), test_scenario::ctx(&mut scenario)); + let clock = clock::create_for_testing(test_scenario::ctx(&mut scenario)); + + reserve::update_price_for_testing( + &mut usdc_reserve, + &clock, + decimal::from(1), + decimal::from(2) + ); + reserve::update_price_for_testing( + &mut sui_reserve, + &clock, + decimal::from(10), + decimal::from(5) + ); + + reserve::set_emode_for_pair( + &mut sui_reserve, + reserve::array_index(&usdc_reserve), + 40, + 60, + ); + + set_emode( + &mut obligation, + &sui_reserve, // deposit + &usdt_reserve, // borrow + ); + + sui::test_utils::destroy(lending_market_id); + sui::test_utils::destroy(usdc_reserve); + sui::test_utils::destroy(usdt_reserve); + sui::test_utils::destroy(sui_reserve); + sui::test_utils::destroy(obligation); + clock::destroy_for_testing(clock); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = EEModeNotValidWithCrossMargin)] + public fun test_emode_cross_borrow_fail() { + use sui::test_scenario::{Self}; + + let owner = @0x26; + let scenario = test_scenario::begin(owner); + let lending_market_id = object::new(test_scenario::ctx(&mut scenario)); + + let usdc_reserve = usdc_reserve(&mut scenario); + let usdt_reserve = usdt_reserve(&mut scenario); + let sui_reserve = sui_reserve(&mut scenario); + + let obligation = create_obligation(object::uid_to_inner(&lending_market_id), test_scenario::ctx(&mut scenario)); + let clock = clock::create_for_testing(test_scenario::ctx(&mut scenario)); + + reserve::update_price_for_testing( + &mut usdc_reserve, + &clock, + decimal::from(1), + decimal::from(2) + ); + reserve::update_price_for_testing( + &mut usdt_reserve, + &clock, + decimal::from(1), + decimal::from(2) + ); + reserve::update_price_for_testing( + &mut sui_reserve, + &clock, + decimal::from(10), + decimal::from(5) + ); + + deposit(&mut obligation, &mut sui_reserve, &clock, 100 * 1_000_000_000); + borrow(&mut obligation, &mut usdc_reserve, &clock, 12_500_000); + borrow(&mut obligation, &mut usdt_reserve, &clock, 12_500_000); + + set_emode( + &mut obligation, + &usdc_reserve, // deposit + &sui_reserve, // borrow + ); + + sui::test_utils::destroy(lending_market_id); + sui::test_utils::destroy(usdc_reserve); + sui::test_utils::destroy(usdt_reserve); + sui::test_utils::destroy(sui_reserve); + sui::test_utils::destroy(obligation); + clock::destroy_for_testing(clock); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = EEModeNotValidWithCrossMargin)] + public fun test_emode_cross_deposit_fail() { + use sui::test_scenario::{Self}; + + let owner = @0x26; + let scenario = test_scenario::begin(owner); + let lending_market_id = object::new(test_scenario::ctx(&mut scenario)); + + let usdc_reserve = usdc_reserve(&mut scenario); + let usdt_reserve = usdt_reserve(&mut scenario); + let sui_reserve = sui_reserve(&mut scenario); + + let obligation = create_obligation(object::uid_to_inner(&lending_market_id), test_scenario::ctx(&mut scenario)); + let clock = clock::create_for_testing(test_scenario::ctx(&mut scenario)); + + reserve::update_price_for_testing( + &mut usdc_reserve, + &clock, + decimal::from(1), + decimal::from(2) + ); + reserve::update_price_for_testing( + &mut usdt_reserve, + &clock, + decimal::from(1), + decimal::from(2) + ); + reserve::update_price_for_testing( + &mut sui_reserve, + &clock, + decimal::from(10), + decimal::from(5) + ); + + deposit(&mut obligation, &mut sui_reserve, &clock, 100 * 1_000_000_000); + deposit(&mut obligation, &mut usdt_reserve, &clock, 100 * 1_000_000_000); + borrow(&mut obligation, &mut usdc_reserve, &clock, 12_500_000); + + set_emode( + &mut obligation, + &usdc_reserve, // deposit + &sui_reserve, // borrow + ); + + sui::test_utils::destroy(lending_market_id); + sui::test_utils::destroy(usdc_reserve); + sui::test_utils::destroy(usdt_reserve); + sui::test_utils::destroy(sui_reserve); + sui::test_utils::destroy(obligation); + clock::destroy_for_testing(clock); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = EEModeObligationBorrowMismatch)] + public fun test_emode_incorrect_borrow_reserve() { + use sui::test_scenario::{Self}; + + let owner = @0x26; + let scenario = test_scenario::begin(owner); + let lending_market_id = object::new(test_scenario::ctx(&mut scenario)); + + let usdc_reserve = usdc_reserve(&mut scenario); + let usdt_reserve = usdt_reserve(&mut scenario); + let sui_reserve = sui_reserve(&mut scenario); + + let obligation = create_obligation(object::uid_to_inner(&lending_market_id), test_scenario::ctx(&mut scenario)); + let clock = clock::create_for_testing(test_scenario::ctx(&mut scenario)); + + reserve::update_price_for_testing( + &mut usdc_reserve, + &clock, + decimal::from(1), + decimal::from(2) + ); + reserve::update_price_for_testing( + &mut usdt_reserve, + &clock, + decimal::from(1), + decimal::from(2) + ); + reserve::update_price_for_testing( + &mut sui_reserve, + &clock, + decimal::from(10), + decimal::from(5) + ); + + reserve::set_emode_for_pair( + &mut sui_reserve, + reserve::array_index(&usdt_reserve), + 40, + 60, + ); + + deposit(&mut obligation, &mut sui_reserve, &clock, 100 * 1_000_000_000); + borrow(&mut obligation, &mut usdc_reserve, &clock, 12_500_000); + + set_emode( + &mut obligation, + &sui_reserve, // deposit + &usdt_reserve, // borrow + ); + + sui::test_utils::destroy(lending_market_id); + sui::test_utils::destroy(usdc_reserve); + sui::test_utils::destroy(usdt_reserve); + sui::test_utils::destroy(sui_reserve); + sui::test_utils::destroy(obligation); + clock::destroy_for_testing(clock); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = EEModeObligationDepositMismatch)] + public fun test_emode_incorrect_deposit_reserve() { + use sui::test_scenario::{Self}; + + let owner = @0x26; + let scenario = test_scenario::begin(owner); + let lending_market_id = object::new(test_scenario::ctx(&mut scenario)); + + let usdc_reserve = usdc_reserve(&mut scenario); + let usdt_reserve = usdt_reserve(&mut scenario); + let sui_reserve = sui_reserve(&mut scenario); + + let obligation = create_obligation(object::uid_to_inner(&lending_market_id), test_scenario::ctx(&mut scenario)); + let clock = clock::create_for_testing(test_scenario::ctx(&mut scenario)); + + reserve::update_price_for_testing( + &mut usdc_reserve, + &clock, + decimal::from(1), + decimal::from(2) + ); + reserve::update_price_for_testing( + &mut usdt_reserve, + &clock, + decimal::from(1), + decimal::from(2) + ); + reserve::update_price_for_testing( + &mut sui_reserve, + &clock, + decimal::from(10), + decimal::from(5) + ); + + reserve::set_emode_for_pair( + &mut sui_reserve, + reserve::array_index(&usdt_reserve), + 40, + 60, + ); + + deposit(&mut obligation, &mut usdc_reserve, &clock, 100 * 1_000_000_000); + borrow(&mut obligation, &mut usdt_reserve, &clock, 12_500_000); + + set_emode( + &mut obligation, + &sui_reserve, // deposit + &usdt_reserve, // borrow + ); + + sui::test_utils::destroy(lending_market_id); + sui::test_utils::destroy(usdc_reserve); + sui::test_utils::destroy(usdt_reserve); sui::test_utils::destroy(sui_reserve); sui::test_utils::destroy(obligation); clock::destroy_for_testing(clock); test_scenario::end(scenario); } -} +} \ No newline at end of file diff --git a/contracts/sources/reserve_config.move b/contracts/sources/reserve_config.move index 8143689..fa9ec39 100644 --- a/contracts/sources/reserve_config.move +++ b/contracts/sources/reserve_config.move @@ -14,6 +14,8 @@ module suilend::reserve_config { const EInvalidReserveConfig: u64 = 0; const EInvalidUtil: u64 = 1; + const ENoEModeConfigForDepositReserve: u64 = 2; + const EBorrowReserveIsNotAnEModePair: u64 = 3; struct EModeKey has copy, store, drop {} @@ -469,13 +471,14 @@ module suilend::reserve_config { reserve_config: &ReserveConfig, reserve_array_index: &u64, ): bool { - let emode_config = get_emode_config(reserve_config); + let emode_config = get_emode_config_checked(reserve_config); vec_map::contains(emode_config, reserve_array_index) } - - public(friend) fun get_emode_config( + + public(friend) fun get_emode_config_checked( reserve_config: &ReserveConfig, ): &VecMap { + assert!(bag::contains(&reserve_config.additional_fields, EModeKey {}), ENoEModeConfigForDepositReserve); bag::borrow(&reserve_config.additional_fields, EModeKey {}) } @@ -497,14 +500,14 @@ module suilend::reserve_config { decimal::from_percent(emode_data.close_ltv_pct) } - public(friend) fun get_emode_data( + public(friend) fun get_emode_data_checked( reserve_config: &ReserveConfig, reserve_array_index: &u64, ): &EModeData { - let emode_config = get_emode_config(reserve_config); + let emode_config = get_emode_config_checked(reserve_config); let has_pair = vec_map::contains(emode_config, reserve_array_index); - assert!(has_pair, 0); + assert!(has_pair, EBorrowReserveIsNotAnEModePair); vec_map::get(emode_config, reserve_array_index) } @@ -737,7 +740,7 @@ module suilend::reserve_config { check_emode_validity(&config, &1); assert!(has_emode_config(&config), 0); - let emode_data = get_emode_data(&config, &1); + let emode_data = get_emode_data_checked(&config, &1); assert_eq(open_ltv_emode(emode_data), decimal::from_percent(60)); assert_eq(close_ltv_emode(emode_data), decimal::from_percent(80)); From f30f544b26d07e4496a8c123a129696c9e578716 Mon Sep 17 00:00:00 2001 From: 0xxgen1 <0xxgen@solend.fi> Date: Fri, 4 Oct 2024 13:13:45 +0100 Subject: [PATCH 9/9] Nit --- contracts/sources/obligation.move | 3 +-- contracts/sources/reserve_config.move | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/contracts/sources/obligation.move b/contracts/sources/obligation.move index 3542f34..3c3127f 100644 --- a/contracts/sources/obligation.move +++ b/contracts/sources/obligation.move @@ -481,8 +481,7 @@ module suilend::obligation { clock: &Clock, max_repay_amount: Decimal, ): Decimal { - let is_emode = is_emode(obligation); - let borrow_weight = if (is_emode) { + let borrow_weight = if (is_emode(obligation)) { decimal::from(1) } else { borrow_weight(config(reserve)) diff --git a/contracts/sources/reserve_config.move b/contracts/sources/reserve_config.move index fa9ec39..24e0e2b 100644 --- a/contracts/sources/reserve_config.move +++ b/contracts/sources/reserve_config.move @@ -438,9 +438,7 @@ module suilend::reserve_config { open_ltv_pct: u8, close_ltv_pct: u8, ) { - let has_emode_field = bag::contains(&reserve_config.additional_fields, EModeKey {}); - - if (!has_emode_field) { + if (!has_emode_config(reserve_config)) { bag::add( &mut reserve_config.additional_fields, EModeKey {},