From 9d8275bce2c0bd4b771007d2120f06837ccd4318 Mon Sep 17 00:00:00 2001 From: 0xxgen1 <0xxgen@solend.fi> Date: Wed, 6 Nov 2024 18:58:17 +0000 Subject: [PATCH 1/7] - reserve config module - lending_market::set_emode_status - obligation::refresh --- contracts/suilend/sources/cell.move | 4 + contracts/suilend/sources/obligation.move | 121 ++++++- contracts/suilend/sources/reserve.move | 25 ++ contracts/suilend/sources/reserve_config.move | 303 +++++++++++++++++- contracts/suilend/tests/obligation_tests.move | 134 ++++++++ 5 files changed, 575 insertions(+), 12 deletions(-) diff --git a/contracts/suilend/sources/cell.move b/contracts/suilend/sources/cell.move index e364f06..799aa81 100644 --- a/contracts/suilend/sources/cell.move +++ b/contracts/suilend/sources/cell.move @@ -17,6 +17,10 @@ module suilend::cell { 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; option::destroy_some(element) diff --git a/contracts/suilend/sources/obligation.move b/contracts/suilend/sources/obligation.move index b536bd5..3edd5a7 100644 --- a/contracts/suilend/sources/obligation.move +++ b/contracts/suilend/sources/obligation.move @@ -8,14 +8,18 @@ module suilend::obligation { use sui::tx_context::{TxContext}; use suilend::reserve::{Self, Reserve, config}; use suilend::reserve_config::{ + Self, open_ltv, close_ltv, borrow_weight, liquidation_bonus, protocol_liquidation_fee, isolated, + open_ltv_emode, + close_ltv_emode, }; use sui::clock::{Clock}; + use sui::dynamic_field::{Self as df}; use suilend::decimal::{Self, Decimal, mul, add, sub, div, gt, lt, min, floor, le, eq, saturating_sub}; use suilend::liquidity_mining::{Self, UserRewardManager, PoolRewardManager}; @@ -29,12 +33,20 @@ module suilend::obligation { const ETooManyBorrows: u64 = 6; const EObligationIsNotForgivable: u64 = 7; const ECannotDepositAndBorrowSameAsset: u64 = 8; + const EEModeNotValidWithCrossMargin: u64 = 9; + const ENoEmodeConfigForGivenDepositReserve : u64 = 10; + const EInvalidEModeDeposit: u64 = 13; + const EInvalidEModeBorrow: u64 = 14; + const EEModeAlreadySet: u64 = 15; // === Constants === const CLOSE_FACTOR_PCT: u8 = 20; const MAX_DEPOSITS: u64 = 5; const MAX_BORROWS: u64 = 5; + // === Dynamic Field Keys === + public struct EModeFlag has store, copy, drop {} + // === public structs === public struct Obligation has key, store { id: UID, @@ -157,6 +169,30 @@ module suilend::obligation { } } + public(package) fun set_emode

( + obligation: &mut Obligation

, + reserves: &mut vector>, + clock: &Clock, + ) { + assert!(!is_emode(obligation), EEModeAlreadySet); + assert!(vector::length(&obligation.borrows) <= 1, EEModeNotValidWithCrossMargin); + assert!(vector::length(&obligation.deposits) <= 1, EEModeNotValidWithCrossMargin); + + if (vector::length(&obligation.deposits) == 1) { + let reserve_array_index = vector::borrow(&obligation.deposits, 0).reserve_array_index; + let deposit_reserve = vector::borrow(reserves, reserve_array_index); + + assert!( + reserve_config::has_emode_config(config(deposit_reserve)), + ENoEmodeConfigForGivenDepositReserve, + ); + + refresh(obligation, reserves, clock); + + }; + + df::add(&mut obligation.id, EModeFlag {}, true); + } /// update the obligation's borrowed amounts and health values. this is /// called by the lending market prior to any borrow, withdraw, or liquidate operation. @@ -169,6 +205,7 @@ module suilend::obligation { let mut deposited_value_usd = decimal::from(0); let mut allowed_borrow_value_usd = decimal::from(0); let mut 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); @@ -189,19 +226,27 @@ module suilend::obligation { deposit.market_value = market_value; deposited_value_usd = add(deposited_value_usd, market_value); + + let (open_ltv, close_ltv) = get_ltvs( + obligation, + deposit_reserve, + is_emode, + ); + allowed_borrow_value_usd = add( allowed_borrow_value_usd, mul( market_value_lower_bound, - open_ltv(config(deposit_reserve)) - ) + open_ltv, + ), ); + unhealthy_borrow_value_usd = add( unhealthy_borrow_value_usd, mul( market_value, - close_ltv(config(deposit_reserve)) - ) + close_ltv, + ), ); i = i + 1; @@ -230,22 +275,29 @@ module suilend::obligation { 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_weight = if (is_emode) { + decimal::from(1) + } else { + borrow_weight(config(borrow_reserve)) + }; + weighted_borrowed_value_usd = add( weighted_borrowed_value_usd, mul( market_value, - borrow_weight(config(borrow_reserve)) + borrow_weight ) ); weighted_borrowed_value_upper_bound_usd = add( weighted_borrowed_value_upper_bound_usd, mul( market_value_upper_bound, - borrow_weight(config(borrow_reserve)) + borrow_weight ) ); @@ -1221,4 +1273,57 @@ module suilend::obligation { user_reward_manager_index } } -} + + fun get_ltvs

( + obligation: &Obligation

, + deposit_reserve: &Reserve

, + is_emode: bool, + ): (Decimal, Decimal) { + if (is_emode) { + // check_normal_ltv_against_emode_ltvs + let borrow_reserve_index = get_single_borrow_array_reserve_if_any(obligation); + + assert!( + reserve_config::has_emode_config(config(deposit_reserve)), + ENoEmodeConfigForGivenDepositReserve, + ); + + if (option::is_none(&borrow_reserve_index)) { + // When obligation is emode but there is no borrows + (open_ltv(config(deposit_reserve)), close_ltv(config(deposit_reserve))) + } else { + // When obligation is emode and there is a borrow + let emode_data = reserve_config::try_get_emode_data( + config(deposit_reserve), + option::borrow(&borrow_reserve_index) + ); + + if (option::is_some(&emode_data)) { + // When obligation is emode and there is a borrow AND there is a matching emode config + (open_ltv_emode(option::borrow(&emode_data)), close_ltv_emode(option::borrow(&emode_data))) + } else { + // When obligation is emode and there is a borrow BUT there is no matching emode config + (open_ltv(config(deposit_reserve)), close_ltv(config(deposit_reserve))) + } + } + } else { + // When obligation normal mode + (open_ltv(config(deposit_reserve)), close_ltv(config(deposit_reserve))) + } + } + + fun is_emode

(obligation: &Obligation

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

( + obligation: &Obligation

, + ): Option { + let len = vector::length(&obligation.borrows); + if (len != 1) { + option::none() + } else { + option::some(vector::borrow(&obligation.borrows, 0).reserve_array_index) + } + } +} \ No newline at end of file diff --git a/contracts/suilend/sources/reserve.move b/contracts/suilend/sources/reserve.move index 929e043..9ba3b3d 100644 --- a/contracts/suilend/sources/reserve.move +++ b/contracts/suilend/sources/reserve.move @@ -1031,4 +1031,29 @@ module suilend::reserve { reserve } + + #[test_only] + public fun set_emode_for_pair

( + reserve: &mut Reserve

, + reserve_array_index: u64, + open_ltv_pct: u8, + close_ltv_pct: u8, + ctx: &mut TxContext, + ) { + let config = cell::get_mut(&mut reserve.config); + + let mut builder = reserve_config::from(config, ctx); + + let emode_data = reserve_config::create_emode_data( + reserve_array_index, + open_ltv_pct, + close_ltv_pct, + ); + + reserve_config::set_emode_ltv_for_borrow(&mut builder, emode_data); + + reserve_config::destroy( + cell::set(&mut reserve.config, reserve_config::build(builder, ctx)) + ); + } } diff --git a/contracts/suilend/sources/reserve_config.move b/contracts/suilend/sources/reserve_config.move index cd13f7e..831665d 100644 --- a/contracts/suilend/sources/reserve_config.move +++ b/contracts/suilend/sources/reserve_config.move @@ -4,12 +4,19 @@ module suilend::reserve_config { use suilend::decimal::{Decimal, Self, add, sub, mul, div, ge, le}; use sui::tx_context::{TxContext}; use sui::bag::{Self, Bag}; + use std::option::{Self, Option}; + use sui::vec_map::{Self, VecMap}; #[test_only] use sui::test_scenario::{Self}; const EInvalidReserveConfig: u64 = 0; const EInvalidUtil: u64 = 1; + const ENoEModeConfigForDepositReserve: u64 = 2; + const ENormalOpenLtvBetterThanEModeLtvs: u64 = 3; + const ENormalCloseLtvBetterThanEModeLtvs: u64 = 4; + + public struct EModeKey has copy, store, drop {} public struct ReserveConfig has store { // risk params @@ -57,6 +64,13 @@ module suilend::reserve_config { fields: Bag } + public struct EModeData has store, copy, drop { + // Corresponding borrow reserve index + 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, @@ -78,7 +92,65 @@ module suilend::reserve_config { close_attributed_borrow_limit_usd: u64, ctx: &mut TxContext ): ReserveConfig { - let config = ReserveConfig { + create_reserve_config_( + open_ltv_pct, + close_ltv_pct, + max_close_ltv_pct, + borrow_weight_bps, + deposit_limit, + borrow_limit, + liquidation_bonus_bps, + max_liquidation_bonus_bps, + deposit_limit_usd, + borrow_limit_usd, + borrow_fee_bps, + spread_fee_bps, + protocol_liquidation_fee_bps, + interest_rate_utils, + interest_rate_aprs, + isolated, + open_attributed_borrow_limit_usd, + close_attributed_borrow_limit_usd, + option::none(), + ctx, + ) + } + + public fun create_emode_data( + reserve_array_index: u64, + open_ltv_pct: u8, + close_ltv_pct: u8, + ): EModeData { + EModeData { + reserve_array_index, + open_ltv_pct, + close_ltv_pct, + } + } + + fun create_reserve_config_( + open_ltv_pct: u8, + close_ltv_pct: u8, + max_close_ltv_pct: u8, + borrow_weight_bps: u64, + deposit_limit: u64, + borrow_limit: u64, + liquidation_bonus_bps: u64, + max_liquidation_bonus_bps: u64, + deposit_limit_usd: u64, + borrow_limit_usd: u64, + borrow_fee_bps: u64, + spread_fee_bps: u64, + protocol_liquidation_fee_bps: u64, + interest_rate_utils: vector, + interest_rate_aprs: vector, + isolated: bool, + open_attributed_borrow_limit_usd: u64, + close_attributed_borrow_limit_usd: u64, + emode_config: Option>, + ctx: &mut TxContext + ): ReserveConfig { + let mut config = ReserveConfig { open_ltv_pct, close_ltv_pct, max_close_ltv_pct, @@ -100,6 +172,14 @@ module suilend::reserve_config { additional_fields: bag::new(ctx) }; + if (option::is_some(&emode_config)) { + bag::add( + &mut config.additional_fields, + EModeKey {}, + option::destroy_some(emode_config), + ); + }; + validate_reserve_config(&config); config } @@ -132,6 +212,7 @@ module suilend::reserve_config { ); validate_utils_and_aprs(&config.interest_rate_utils, &config.interest_rate_aprs); + validate_emode_ltvs(config); } fun validate_utils_and_aprs(utils: &vector, aprs: &vector) { @@ -156,6 +237,21 @@ module suilend::reserve_config { i = i + 1; } } + + fun validate_emode_ltvs(config: &ReserveConfig) { + if (bag::contains(&config.additional_fields, EModeKey {})) { + let emode_ltvs: &VecMap = bag::borrow(&config.additional_fields, EModeKey {}); + + let mut keys = vec_map::keys(emode_ltvs); + + while (vector::length(&keys) > 0) { + let emode = vec_map::get(emode_ltvs, &vector::pop_back(&mut keys)); + + assert!(config.open_ltv_pct < emode.open_ltv_pct, ENormalOpenLtvBetterThanEModeLtvs); + assert!(config.close_ltv_pct < emode.close_ltv_pct, ENormalCloseLtvBetterThanEModeLtvs); + }; + }; + } public fun open_ltv(config: &ReserveConfig): Decimal { decimal::from_percent(config.open_ltv_pct) @@ -264,9 +360,18 @@ module suilend::reserve_config { isolated: _, open_attributed_borrow_limit_usd: _, close_attributed_borrow_limit_usd: _, - additional_fields + mut additional_fields } = config; + let has_emode_field = bag::contains(&additional_fields, EModeKey {}); + + if (has_emode_field) { + let _emode_config: VecMap = bag::remove( + &mut additional_fields, + EModeKey {}, + ); + }; + bag::destroy_empty(additional_fields); } @@ -293,6 +398,10 @@ module suilend::reserve_config { set_open_attributed_borrow_limit_usd(&mut builder, config.open_attributed_borrow_limit_usd); set_close_attributed_borrow_limit_usd(&mut builder, config.close_attributed_borrow_limit_usd); + if (bag::contains(&config.additional_fields, EModeKey {})) { + set_emode_ltv_for_borrow(&mut builder, *bag::borrow(&config.additional_fields, EModeKey {})) + }; + builder } @@ -377,8 +486,35 @@ module suilend::reserve_config { set(builder, b"close_attributed_borrow_limit_usd", close_attributed_borrow_limit_usd); } + public fun set_emode_ltv_for_borrow( + builder : &mut ReserveConfigBuilder, + emode_data: EModeData + ) { + if (bag::contains(&builder.fields, b"emode")) { + let emode_config: &mut VecMap = bag::borrow_mut(&mut builder.fields, b"emode"); + if (vec_map::contains(emode_config, &emode_data.reserve_array_index)) { + let data: &mut EModeData = bag::borrow_mut(&mut builder.fields, emode_data.reserve_array_index); + *data = emode_data; + } else { + vec_map::insert(emode_config, emode_data.reserve_array_index, emode_data) + }; + } else { + let mut emode_config: VecMap = vec_map::empty(); + vec_map::insert(&mut emode_config, emode_data.reserve_array_index, emode_data); + + bag::add(&mut builder.fields, b"emode", emode_config); + } + } + public fun build(mut builder: ReserveConfigBuilder, tx_context: &mut TxContext): ReserveConfig { - let config = create_reserve_config( + + let emode_config = if (bag::contains(&builder.fields, b"emode")) { + option::some(bag::remove(&mut builder.fields, b"emode")) + } else { + option::none() + }; + + let config = create_reserve_config_( bag::remove(&mut builder.fields, b"open_ltv_pct"), bag::remove(&mut builder.fields, b"close_ltv_pct"), bag::remove(&mut builder.fields, b"max_close_ltv_pct"), @@ -397,6 +533,7 @@ module suilend::reserve_config { bag::remove(&mut builder.fields, b"isolated"), bag::remove(&mut builder.fields, b"open_attributed_borrow_limit_usd"), bag::remove(&mut builder.fields, b"close_attributed_borrow_limit_usd"), + emode_config, tx_context ); @@ -406,6 +543,55 @@ module suilend::reserve_config { } + // === eMode Package Functions == + + public(package) fun check_has_emode_pair( + reserve_config: &ReserveConfig, + reserve_array_index: &u64, + ): bool { + let emode_config = get_emode_config_checked(reserve_config); + vec_map::contains(emode_config, reserve_array_index) + } + + public(package) 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 {}) + } + + public(package) fun has_emode_config( + reserve_config: &ReserveConfig, + ): bool { + bag::contains(&reserve_config.additional_fields, EModeKey {}) + } + + public(package) fun open_ltv_emode( + emode_data: &EModeData, + ): Decimal { + decimal::from_percent(emode_data.open_ltv_pct) + } + + public(package) fun close_ltv_emode( + emode_data: &EModeData, + ): Decimal { + decimal::from_percent(emode_data.close_ltv_pct) + } + + public(package) fun try_get_emode_data( + reserve_config: &ReserveConfig, + reserve_array_index: &u64, + ): Option { + let emode_config = get_emode_config_checked(reserve_config); + let has_pair = vec_map::contains(emode_config, reserve_array_index); + + if (has_pair) { + vec_map::try_get(emode_config, reserve_array_index) + } else { + option::none() + } + } + // === Tests == #[test] fun test_calculate_apr() { @@ -587,4 +773,113 @@ module suilend::reserve_config { config } -} + #[test] + fun test_emode_reserve_config() { + use sui::test_utils::assert_eq; + + let owner = @0x26; + let mut scenario = test_scenario::begin(owner); + + let mut utils = vector::empty(); + vector::push_back(&mut utils, 0); + vector::push_back(&mut utils, 100); + + let mut aprs = vector::empty(); + vector::push_back(&mut aprs, 0); + vector::push_back(&mut aprs, 100); + + let emode_ltvs = create_emode_data( + 1, + 60, + 80, + ); + let mut emode_config = vec_map::empty(); + vec_map::insert(&mut emode_config, 1, emode_ltvs); + + let config = create_reserve_config_( + 10, + 10, + 10, + 10_000, + 1, + 1, + 5, + 5, + 100000, + 100000, + 10, + 2000, + 30, + utils, + aprs, + false, + 0, + 0, + option::some(emode_config), + test_scenario::ctx(&mut scenario) + ); + + check_has_emode_pair(&config, &1); + + assert!(has_emode_config(&config), 0); + let emode_data = option::destroy_some(try_get_emode_data(&config, &1)); + 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); + } + + #[test] + fun test_fail_emode_validity() { + use sui::test_utils::assert_eq; + + let owner = @0x26; + let mut scenario = test_scenario::begin(owner); + + let mut utils = vector::empty(); + vector::push_back(&mut utils, 0); + vector::push_back(&mut utils, 100); + + let mut aprs = vector::empty(); + vector::push_back(&mut aprs, 0); + vector::push_back(&mut aprs, 100); + + let emode_ltvs = create_emode_data( + 1, + 60, + 80, + ); + let mut emode_config = vec_map::empty(); + vec_map::insert(&mut emode_config, 1, emode_ltvs); + + let config = create_reserve_config_( + 10, + 10, + 10, + 10_000, + 1, + 1, + 5, + 5, + 100000, + 100000, + 10, + 2000, + 30, + utils, + aprs, + false, + 0, + 0, + option::some(emode_config), + test_scenario::ctx(&mut scenario) + ); + + assert_eq(check_has_emode_pair(&config, &2), false); + + + destroy(config); + test_scenario::end(scenario); + } +} \ No newline at end of file diff --git a/contracts/suilend/tests/obligation_tests.move b/contracts/suilend/tests/obligation_tests.move index aa0c970..4f0a904 100644 --- a/contracts/suilend/tests/obligation_tests.move +++ b/contracts/suilend/tests/obligation_tests.move @@ -2264,4 +2264,138 @@ module suilend::obligation_tests { sui::test_utils::destroy(obligation); test_scenario::end(scenario); } + + #[test] + #[expected_failure(abort_code = suilend::obligation::ENoEmodeConfigForGivenDepositReserve)] + public fun test_set_emode_invalid_deposit_reserve() { + use sui::test_scenario::{Self}; + + let owner = @0x26; + let mut scenario = test_scenario::begin(owner); + let lending_market_id = object::new(test_scenario::ctx(&mut scenario)); + + let mut reserves = reserves(&mut scenario); + + let mut 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( + get_reserve_mut(&mut reserves), + &clock, + decimal::from(1), + decimal::from(2) + ); + reserve::update_price_for_testing( + get_reserve_mut(&mut reserves), + &clock, + decimal::from(10), + decimal::from(5) + ); + + deposit(&mut obligation, get_reserve_mut(&mut reserves), &clock, 12_500_000); + + obligation.set_emode(&mut reserves, &clock); + + sui::test_utils::destroy(lending_market_id); + sui::test_utils::destroy(reserves); + sui::test_utils::destroy(obligation); + clock::destroy_for_testing(clock); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = suilend::obligation::EEModeAlreadySet)] + public fun test_emode_already_set() { + use sui::test_scenario::{Self}; + + let owner = @0x26; + let mut scenario = test_scenario::begin(owner); + let lending_market_id = object::new(test_scenario::ctx(&mut scenario)); + + let mut reserves = reserves(&mut scenario); + + let mut 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( + get_reserve_mut(&mut reserves), + &clock, + decimal::from(1), + decimal::from(2) + ); + reserve::update_price_for_testing( + get_reserve_mut(&mut reserves), + &clock, + decimal::from(10), + decimal::from(5) + ); + + let usdc_index = get_reserve_array_index(&reserves); + reserve::set_emode_for_pair( + get_reserve_mut(&mut reserves), + usdc_index, + 40, + 60, + test_scenario::ctx(&mut scenario) + ); + + obligation.set_emode(&mut reserves, &clock); + obligation.set_emode(&mut reserves, &clock); + + sui::test_utils::destroy(lending_market_id); + sui::test_utils::destroy(reserves); + sui::test_utils::destroy(obligation); + clock::destroy_for_testing(clock); + test_scenario::end(scenario); + } + + #[test] + public fun test_emode_refresh_after_toggling_emode() { + use sui::test_scenario::{Self}; + + let owner = @0x26; + let mut scenario = test_scenario::begin(owner); + let lending_market_id = object::new(test_scenario::ctx(&mut scenario)); + + let mut reserves = reserves(&mut scenario); + + let mut 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( + get_reserve_mut(&mut reserves), + &clock, + decimal::from(1), + decimal::from(2) + ); + reserve::update_price_for_testing( + get_reserve_mut(&mut reserves), + &clock, + decimal::from(10), + decimal::from(5) + ); + + let borrow_idx = get_reserve_array_index(&reserves); + reserve::set_emode_for_pair( + get_reserve_mut(&mut reserves), + borrow_idx, + 40, + 60, + test_scenario::ctx(&mut scenario) + ); + + deposit(&mut obligation, get_reserve_mut(&mut reserves), &clock, 100 * 1_000_000_000); + borrow(&mut obligation, get_reserve_mut(&mut reserves), &clock, 12_500_000); + borrow(&mut obligation, get_reserve_mut(&mut reserves), &clock, 12_500_000); + + obligation.set_emode(&mut reserves, &clock); + + refresh(&mut obligation, &mut reserves, &clock); + + sui::test_utils::destroy(lending_market_id); + sui::test_utils::destroy(reserves); + sui::test_utils::destroy(obligation); + clock::destroy_for_testing(clock); + test_scenario::end(scenario); + } } From 995015965e087b25a61705413a762a94b4218274 Mon Sep 17 00:00:00 2001 From: 0xxgen1 <0xxgen@solend.fi> Date: Fri, 8 Nov 2024 01:17:20 +0000 Subject: [PATCH 2/7] Review items --- contracts/suilend/sources/cell.move | 4 - contracts/suilend/sources/lending_market.move | 20 ++ contracts/suilend/sources/obligation.move | 127 +++++++++-- contracts/suilend/sources/reserve.move | 25 --- contracts/suilend/sources/reserve_config.move | 205 +++++++++++++++--- contracts/suilend/tests/obligation_tests.move | 68 ++---- 6 files changed, 333 insertions(+), 116 deletions(-) diff --git a/contracts/suilend/sources/cell.move b/contracts/suilend/sources/cell.move index 799aa81..e364f06 100644 --- a/contracts/suilend/sources/cell.move +++ b/contracts/suilend/sources/cell.move @@ -17,10 +17,6 @@ module suilend::cell { 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; option::destroy_some(element) diff --git a/contracts/suilend/sources/lending_market.move b/contracts/suilend/sources/lending_market.move index 875ec74..a81cac2 100644 --- a/contracts/suilend/sources/lending_market.move +++ b/contracts/suilend/sources/lending_market.move @@ -378,6 +378,26 @@ module suilend::lending_market { coin::from_balance(receive_balance, ctx) } + /// Set emode for obligation - T is the deposit coin type + public fun set_emode

( + lending_market: &mut LendingMarket

, + obligation_owner_cap: &ObligationOwnerCap

, + clock: &Clock + ) { + assert!(lending_market.version == CURRENT_VERSION, EIncorrectVersion); + + let obligation = object_table::borrow_mut( + &mut lending_market.obligations, + obligation_owner_cap.obligation_id + ); + + obligation::set_emode( + obligation, + &mut lending_market.reserves, + clock, + ); + } + public fun withdraw_ctokens( lending_market: &mut LendingMarket

, reserve_array_index: u64, diff --git a/contracts/suilend/sources/obligation.move b/contracts/suilend/sources/obligation.move index 3edd5a7..7de0321 100644 --- a/contracts/suilend/sources/obligation.move +++ b/contracts/suilend/sources/obligation.move @@ -178,18 +178,7 @@ module suilend::obligation { assert!(vector::length(&obligation.borrows) <= 1, EEModeNotValidWithCrossMargin); assert!(vector::length(&obligation.deposits) <= 1, EEModeNotValidWithCrossMargin); - if (vector::length(&obligation.deposits) == 1) { - let reserve_array_index = vector::borrow(&obligation.deposits, 0).reserve_array_index; - let deposit_reserve = vector::borrow(reserves, reserve_array_index); - - assert!( - reserve_config::has_emode_config(config(deposit_reserve)), - ENoEmodeConfigForGivenDepositReserve, - ); - - refresh(obligation, reserves, clock); - - }; + refresh(obligation, reserves, clock); df::add(&mut obligation.id, EModeFlag {}, true); } @@ -206,6 +195,114 @@ module suilend::obligation { let mut allowed_borrow_value_usd = decimal::from(0); let mut unhealthy_borrow_value_usd = decimal::from(0); let is_emode = is_emode(obligation); + if (is_emode) { + obligation.refresh_emode(reserves, is_emode, clock) + + } else { + 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); + allowed_borrow_value_usd = add( + allowed_borrow_value_usd, + mul( + market_value_lower_bound, + open_ltv(config(deposit_reserve)) + ) + ); + unhealthy_borrow_value_usd = add( + unhealthy_borrow_value_usd, + mul( + market_value, + close_ltv(config(deposit_reserve)) + ) + ); + + 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 mut i = 0; + let mut unweighted_borrowed_value_usd = decimal::from(0); + let mut weighted_borrowed_value_usd = decimal::from(0); + let mut weighted_borrowed_value_upper_bound_usd = decimal::from(0); + let mut borrowing_isolated_asset = false; + + 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); + 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)) + ) + ); + + 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; + }; + } + + fun refresh_emode

( + obligation: &mut Obligation

, + reserves: &mut vector>, + is_emode: bool, + clock: &Clock + ) { + let mut i = 0; + let mut deposited_value_usd = decimal::from(0); + let mut allowed_borrow_value_usd = decimal::from(0); + let mut unhealthy_borrow_value_usd = decimal::from(0); while (i < vector::length(&obligation.deposits)) { let deposit = vector::borrow_mut(&mut obligation.deposits, i); @@ -1293,14 +1390,14 @@ module suilend::obligation { (open_ltv(config(deposit_reserve)), close_ltv(config(deposit_reserve))) } else { // When obligation is emode and there is a borrow - let emode_data = reserve_config::try_get_emode_data( + let emode_entry = reserve_config::try_get_emode_entry( config(deposit_reserve), option::borrow(&borrow_reserve_index) ); - if (option::is_some(&emode_data)) { + if (option::is_some(&emode_entry)) { // When obligation is emode and there is a borrow AND there is a matching emode config - (open_ltv_emode(option::borrow(&emode_data)), close_ltv_emode(option::borrow(&emode_data))) + (open_ltv_emode(option::borrow(&emode_entry)), close_ltv_emode(option::borrow(&emode_entry))) } else { // When obligation is emode and there is a borrow BUT there is no matching emode config (open_ltv(config(deposit_reserve)), close_ltv(config(deposit_reserve))) diff --git a/contracts/suilend/sources/reserve.move b/contracts/suilend/sources/reserve.move index 9ba3b3d..929e043 100644 --- a/contracts/suilend/sources/reserve.move +++ b/contracts/suilend/sources/reserve.move @@ -1031,29 +1031,4 @@ module suilend::reserve { reserve } - - #[test_only] - public fun set_emode_for_pair

( - reserve: &mut Reserve

, - reserve_array_index: u64, - open_ltv_pct: u8, - close_ltv_pct: u8, - ctx: &mut TxContext, - ) { - let config = cell::get_mut(&mut reserve.config); - - let mut builder = reserve_config::from(config, ctx); - - let emode_data = reserve_config::create_emode_data( - reserve_array_index, - open_ltv_pct, - close_ltv_pct, - ); - - reserve_config::set_emode_ltv_for_borrow(&mut builder, emode_data); - - reserve_config::destroy( - cell::set(&mut reserve.config, reserve_config::build(builder, ctx)) - ); - } } diff --git a/contracts/suilend/sources/reserve_config.move b/contracts/suilend/sources/reserve_config.move index 831665d..227235a 100644 --- a/contracts/suilend/sources/reserve_config.move +++ b/contracts/suilend/sources/reserve_config.move @@ -64,7 +64,7 @@ module suilend::reserve_config { fields: Bag } - public struct EModeData has store, copy, drop { + public struct EModeEntry has store, copy, drop { // Corresponding borrow reserve index reserve_array_index: u64, open_ltv_pct: u8, @@ -116,12 +116,12 @@ module suilend::reserve_config { ) } - public fun create_emode_data( + public fun create_emode_entry( reserve_array_index: u64, open_ltv_pct: u8, close_ltv_pct: u8, - ): EModeData { - EModeData { + ): EModeEntry { + EModeEntry { reserve_array_index, open_ltv_pct, close_ltv_pct, @@ -147,7 +147,7 @@ module suilend::reserve_config { isolated: bool, open_attributed_borrow_limit_usd: u64, close_attributed_borrow_limit_usd: u64, - emode_config: Option>, + emode_config: Option>, ctx: &mut TxContext ): ReserveConfig { let mut config = ReserveConfig { @@ -240,7 +240,7 @@ module suilend::reserve_config { fun validate_emode_ltvs(config: &ReserveConfig) { if (bag::contains(&config.additional_fields, EModeKey {})) { - let emode_ltvs: &VecMap = bag::borrow(&config.additional_fields, EModeKey {}); + let emode_ltvs: &VecMap = bag::borrow(&config.additional_fields, EModeKey {}); let mut keys = vec_map::keys(emode_ltvs); @@ -366,7 +366,7 @@ module suilend::reserve_config { let has_emode_field = bag::contains(&additional_fields, EModeKey {}); if (has_emode_field) { - let _emode_config: VecMap = bag::remove( + let _emode_config: VecMap = bag::remove( &mut additional_fields, EModeKey {}, ); @@ -399,7 +399,7 @@ module suilend::reserve_config { set_close_attributed_borrow_limit_usd(&mut builder, config.close_attributed_borrow_limit_usd); if (bag::contains(&config.additional_fields, EModeKey {})) { - set_emode_ltv_for_borrow(&mut builder, *bag::borrow(&config.additional_fields, EModeKey {})) + set_emode(&mut builder, config.additional_fields.borrow(EModeKey {})) }; builder @@ -486,21 +486,41 @@ module suilend::reserve_config { set(builder, b"close_attributed_borrow_limit_usd", close_attributed_borrow_limit_usd); } + fun set_emode( + builder : &mut ReserveConfigBuilder, + emode_config: &VecMap, + ) { + if (!bag::contains(&builder.fields, b"emode")) { + let emode_config_copy: VecMap = vec_map::empty(); + bag::add(&mut builder.fields, b"emode", emode_config_copy); + }; + + let emode_config_copy: &mut VecMap = bag::borrow_mut( + &mut builder.fields, b"emode" + ); + + let keys = emode_config.keys(); + + keys.do_ref!(|key| { + emode_config_copy.insert(*key, *emode_config.get(key)); + }); + } + public fun set_emode_ltv_for_borrow( builder : &mut ReserveConfigBuilder, - emode_data: EModeData + emode_entry: EModeEntry ) { if (bag::contains(&builder.fields, b"emode")) { - let emode_config: &mut VecMap = bag::borrow_mut(&mut builder.fields, b"emode"); - if (vec_map::contains(emode_config, &emode_data.reserve_array_index)) { - let data: &mut EModeData = bag::borrow_mut(&mut builder.fields, emode_data.reserve_array_index); - *data = emode_data; + let emode_config: &mut VecMap = bag::borrow_mut(&mut builder.fields, b"emode"); + if (vec_map::contains(emode_config, &emode_entry.reserve_array_index)) { + let data: &mut EModeEntry = bag::borrow_mut(&mut builder.fields, emode_entry.reserve_array_index); + *data = emode_entry; } else { - vec_map::insert(emode_config, emode_data.reserve_array_index, emode_data) + vec_map::insert(emode_config, emode_entry.reserve_array_index, emode_entry) }; } else { - let mut emode_config: VecMap = vec_map::empty(); - vec_map::insert(&mut emode_config, emode_data.reserve_array_index, emode_data); + let mut emode_config: VecMap = vec_map::empty(); + vec_map::insert(&mut emode_config, emode_entry.reserve_array_index, emode_entry); bag::add(&mut builder.fields, b"emode", emode_config); } @@ -555,7 +575,7 @@ module suilend::reserve_config { public(package) fun get_emode_config_checked( reserve_config: &ReserveConfig, - ): &VecMap { + ): &VecMap { assert!(bag::contains(&reserve_config.additional_fields, EModeKey {}), ENoEModeConfigForDepositReserve); bag::borrow(&reserve_config.additional_fields, EModeKey {}) } @@ -567,21 +587,21 @@ module suilend::reserve_config { } public(package) fun open_ltv_emode( - emode_data: &EModeData, + emode_entry: &EModeEntry, ): Decimal { - decimal::from_percent(emode_data.open_ltv_pct) + decimal::from_percent(emode_entry.open_ltv_pct) } public(package) fun close_ltv_emode( - emode_data: &EModeData, + emode_entry: &EModeEntry, ): Decimal { - decimal::from_percent(emode_data.close_ltv_pct) + decimal::from_percent(emode_entry.close_ltv_pct) } - public(package) fun try_get_emode_data( + public(package) fun try_get_emode_entry( reserve_config: &ReserveConfig, reserve_array_index: &u64, - ): Option { + ): Option { let emode_config = get_emode_config_checked(reserve_config); let has_pair = vec_map::contains(emode_config, reserve_array_index); @@ -788,7 +808,7 @@ module suilend::reserve_config { vector::push_back(&mut aprs, 0); vector::push_back(&mut aprs, 100); - let emode_ltvs = create_emode_data( + let emode_ltvs = create_emode_entry( 1, 60, 80, @@ -822,9 +842,9 @@ module suilend::reserve_config { check_has_emode_pair(&config, &1); assert!(has_emode_config(&config), 0); - let emode_data = option::destroy_some(try_get_emode_data(&config, &1)); - assert_eq(open_ltv_emode(&emode_data), decimal::from_percent(60)); - assert_eq(close_ltv_emode(&emode_data), decimal::from_percent(80)); + let emode_entry = option::destroy_some(try_get_emode_entry(&config, &1)); + assert_eq(open_ltv_emode(&emode_entry), decimal::from_percent(60)); + assert_eq(close_ltv_emode(&emode_entry), decimal::from_percent(80)); destroy(config); test_scenario::end(scenario); @@ -845,7 +865,7 @@ module suilend::reserve_config { vector::push_back(&mut aprs, 0); vector::push_back(&mut aprs, 100); - let emode_ltvs = create_emode_data( + let emode_ltvs = create_emode_entry( 1, 60, 80, @@ -882,4 +902,133 @@ module suilend::reserve_config { destroy(config); test_scenario::end(scenario); } + + #[test] + fun test_emode_reserve_config_builder_with_emode() { + use sui::test_utils::assert_eq; + + let owner = @0x26; + let mut scenario = test_scenario::begin(owner); + + let mut utils = vector::empty(); + vector::push_back(&mut utils, 0); + vector::push_back(&mut utils, 100); + + let mut aprs = vector::empty(); + vector::push_back(&mut aprs, 0); + vector::push_back(&mut aprs, 100); + + let emode_ltvs = create_emode_entry( + 1, + 60, + 80, + ); + let mut emode_config = vec_map::empty(); + vec_map::insert(&mut emode_config, 1, emode_ltvs); + + let config = create_reserve_config_( + 10, + 10, + 10, + 10_000, + 1, + 1, + 5, + 5, + 100000, + 100000, + 10, + 2000, + 30, + utils, + aprs, + false, + 0, + 0, + option::some(emode_config), + test_scenario::ctx(&mut scenario) + ); + + check_has_emode_pair(&config, &1); + + assert!(has_emode_config(&config), 0); + let emode_entry = option::destroy_some(try_get_emode_entry(&config, &1)); + assert_eq(open_ltv_emode(&emode_entry), decimal::from_percent(60)); + assert_eq(close_ltv_emode(&emode_entry), decimal::from_percent(80)); + + let builder = from(&config, test_scenario::ctx(&mut scenario)); + let new_config = builder.build(test_scenario::ctx(&mut scenario)); + + check_has_emode_pair(&new_config, &1); + + assert!(has_emode_config(&new_config), 0); + let emode_entry = option::destroy_some(try_get_emode_entry(&new_config, &1)); + assert_eq(open_ltv_emode(&emode_entry), decimal::from_percent(60)); + assert_eq(close_ltv_emode(&emode_entry), decimal::from_percent(80)); + + destroy(config); + destroy(new_config); + test_scenario::end(scenario); + } + + #[test] + fun test_emode_reserve_config_builder_adding_emode_at_build_time() { + use sui::test_utils::assert_eq; + + let owner = @0x26; + let mut scenario = test_scenario::begin(owner); + + let mut utils = vector::empty(); + vector::push_back(&mut utils, 0); + vector::push_back(&mut utils, 100); + + let mut 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, + option::none(), + test_scenario::ctx(&mut scenario) + ); + + let mut builder = from(&config, test_scenario::ctx(&mut scenario)); + + let emode_ltvs = create_emode_entry( + 1, + 60, + 80, + ); + + builder.set_emode_ltv_for_borrow(emode_ltvs); + let new_config = builder.build(test_scenario::ctx(&mut scenario)); + + check_has_emode_pair(&new_config, &1); + + assert!(has_emode_config(&new_config), 0); + let emode_entry = option::destroy_some(try_get_emode_entry(&new_config, &1)); + assert_eq(open_ltv_emode(&emode_entry), decimal::from_percent(60)); + assert_eq(close_ltv_emode(&emode_entry), decimal::from_percent(80)); + + destroy(config); + destroy(new_config); + test_scenario::end(scenario); + } } \ No newline at end of file diff --git a/contracts/suilend/tests/obligation_tests.move b/contracts/suilend/tests/obligation_tests.move index 4f0a904..e1650d2 100644 --- a/contracts/suilend/tests/obligation_tests.move +++ b/contracts/suilend/tests/obligation_tests.move @@ -2265,44 +2265,6 @@ module suilend::obligation_tests { test_scenario::end(scenario); } - #[test] - #[expected_failure(abort_code = suilend::obligation::ENoEmodeConfigForGivenDepositReserve)] - public fun test_set_emode_invalid_deposit_reserve() { - use sui::test_scenario::{Self}; - - let owner = @0x26; - let mut scenario = test_scenario::begin(owner); - let lending_market_id = object::new(test_scenario::ctx(&mut scenario)); - - let mut reserves = reserves(&mut scenario); - - let mut 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( - get_reserve_mut(&mut reserves), - &clock, - decimal::from(1), - decimal::from(2) - ); - reserve::update_price_for_testing( - get_reserve_mut(&mut reserves), - &clock, - decimal::from(10), - decimal::from(5) - ); - - deposit(&mut obligation, get_reserve_mut(&mut reserves), &clock, 12_500_000); - - obligation.set_emode(&mut reserves, &clock); - - sui::test_utils::destroy(lending_market_id); - sui::test_utils::destroy(reserves); - sui::test_utils::destroy(obligation); - clock::destroy_for_testing(clock); - test_scenario::end(scenario); - } - #[test] #[expected_failure(abort_code = suilend::obligation::EEModeAlreadySet)] public fun test_emode_already_set() { @@ -2331,14 +2293,19 @@ module suilend::obligation_tests { ); let usdc_index = get_reserve_array_index(&reserves); - reserve::set_emode_for_pair( - get_reserve_mut(&mut reserves), + let sui_reserve = get_reserve_mut(&mut reserves); + + let mut builder = sui_reserve.config().from(test_scenario::ctx(&mut scenario)); + let emode_ltvs = reserve_config::create_emode_entry( usdc_index, 40, 60, - test_scenario::ctx(&mut scenario) ); + builder.set_emode_ltv_for_borrow(emode_ltvs); + let new_config = builder.build(test_scenario::ctx(&mut scenario)); + sui_reserve.update_reserve_config(new_config); + obligation.set_emode(&mut reserves, &clock); obligation.set_emode(&mut reserves, &clock); @@ -2376,14 +2343,27 @@ module suilend::obligation_tests { ); let borrow_idx = get_reserve_array_index(&reserves); - reserve::set_emode_for_pair( - get_reserve_mut(&mut reserves), + // reserve::set_emode_for_pair( + // get_reserve_mut(&mut reserves), + // borrow_idx, + // 40, + // 60, + // test_scenario::ctx(&mut scenario) + // ); + + let sui_reserve = get_reserve_mut(&mut reserves); + + let mut builder = sui_reserve.config().from(test_scenario::ctx(&mut scenario)); + let emode_ltvs = reserve_config::create_emode_entry( borrow_idx, 40, 60, - test_scenario::ctx(&mut scenario) ); + builder.set_emode_ltv_for_borrow(emode_ltvs); + let new_config = builder.build(test_scenario::ctx(&mut scenario)); + sui_reserve.update_reserve_config(new_config); + deposit(&mut obligation, get_reserve_mut(&mut reserves), &clock, 100 * 1_000_000_000); borrow(&mut obligation, get_reserve_mut(&mut reserves), &clock, 12_500_000); borrow(&mut obligation, get_reserve_mut(&mut reserves), &clock, 12_500_000); From 6dad8d572f98e5101f82384d872604e0f38d316c Mon Sep 17 00:00:00 2001 From: 0xxgen1 <0xxgen@solend.fi> Date: Fri, 8 Nov 2024 01:37:43 +0000 Subject: [PATCH 3/7] Review items --- contracts/suilend/sources/reserve_config.move | 31 +++++++++---------- contracts/suilend/tests/obligation_tests.move | 8 ----- 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/contracts/suilend/sources/reserve_config.move b/contracts/suilend/sources/reserve_config.move index 227235a..189e462 100644 --- a/contracts/suilend/sources/reserve_config.move +++ b/contracts/suilend/sources/reserve_config.move @@ -565,27 +565,27 @@ module suilend::reserve_config { // === eMode Package Functions == - public(package) fun check_has_emode_pair( + public(package) fun has_emode_config( + reserve_config: &ReserveConfig, + ): bool { + bag::contains(&reserve_config.additional_fields, EModeKey {}) + } + + public(package) fun has_emode_pair( reserve_config: &ReserveConfig, reserve_array_index: &u64, ): bool { - let emode_config = get_emode_config_checked(reserve_config); + let emode_config = get_emode_config(reserve_config); vec_map::contains(emode_config, reserve_array_index) } - public(package) fun get_emode_config_checked( + public(package) fun get_emode_config( reserve_config: &ReserveConfig, ): &VecMap { assert!(bag::contains(&reserve_config.additional_fields, EModeKey {}), ENoEModeConfigForDepositReserve); bag::borrow(&reserve_config.additional_fields, EModeKey {}) } - public(package) fun has_emode_config( - reserve_config: &ReserveConfig, - ): bool { - bag::contains(&reserve_config.additional_fields, EModeKey {}) - } - public(package) fun open_ltv_emode( emode_entry: &EModeEntry, ): Decimal { @@ -602,7 +602,7 @@ module suilend::reserve_config { reserve_config: &ReserveConfig, reserve_array_index: &u64, ): Option { - let emode_config = get_emode_config_checked(reserve_config); + let emode_config = get_emode_config(reserve_config); let has_pair = vec_map::contains(emode_config, reserve_array_index); if (has_pair) { @@ -839,8 +839,7 @@ module suilend::reserve_config { test_scenario::ctx(&mut scenario) ); - check_has_emode_pair(&config, &1); - + assert!(has_emode_pair(&config, &1), 0); assert!(has_emode_config(&config), 0); let emode_entry = option::destroy_some(try_get_emode_entry(&config, &1)); assert_eq(open_ltv_emode(&emode_entry), decimal::from_percent(60)); @@ -896,7 +895,7 @@ module suilend::reserve_config { test_scenario::ctx(&mut scenario) ); - assert_eq(check_has_emode_pair(&config, &2), false); + assert_eq(has_emode_pair(&config, &2), false); destroy(config); @@ -949,7 +948,7 @@ module suilend::reserve_config { test_scenario::ctx(&mut scenario) ); - check_has_emode_pair(&config, &1); + assert!(has_emode_pair(&config, &1), 0); assert!(has_emode_config(&config), 0); let emode_entry = option::destroy_some(try_get_emode_entry(&config, &1)); @@ -959,7 +958,7 @@ module suilend::reserve_config { let builder = from(&config, test_scenario::ctx(&mut scenario)); let new_config = builder.build(test_scenario::ctx(&mut scenario)); - check_has_emode_pair(&new_config, &1); + assert!(has_emode_pair(&new_config, &1), 0); assert!(has_emode_config(&new_config), 0); let emode_entry = option::destroy_some(try_get_emode_entry(&new_config, &1)); @@ -1020,7 +1019,7 @@ module suilend::reserve_config { builder.set_emode_ltv_for_borrow(emode_ltvs); let new_config = builder.build(test_scenario::ctx(&mut scenario)); - check_has_emode_pair(&new_config, &1); + assert!(has_emode_pair(&new_config, &1), 0); assert!(has_emode_config(&new_config), 0); let emode_entry = option::destroy_some(try_get_emode_entry(&new_config, &1)); diff --git a/contracts/suilend/tests/obligation_tests.move b/contracts/suilend/tests/obligation_tests.move index e1650d2..9922d2a 100644 --- a/contracts/suilend/tests/obligation_tests.move +++ b/contracts/suilend/tests/obligation_tests.move @@ -2343,14 +2343,6 @@ module suilend::obligation_tests { ); let borrow_idx = get_reserve_array_index(&reserves); - // reserve::set_emode_for_pair( - // get_reserve_mut(&mut reserves), - // borrow_idx, - // 40, - // 60, - // test_scenario::ctx(&mut scenario) - // ); - let sui_reserve = get_reserve_mut(&mut reserves); let mut builder = sui_reserve.config().from(test_scenario::ctx(&mut scenario)); From d7cae351be6bd773d87c84806787b5fe31b03aea Mon Sep 17 00:00:00 2001 From: 0xxgen1 <0xxgen@solend.fi> Date: Fri, 8 Nov 2024 12:03:27 +0000 Subject: [PATCH 4/7] - Fixed assertion over emode ltv thresholds - Refresh after adding the emode flag - Ability to toggle emode on and off - Simplified refresh emode - Fixed borrow weight when no emode pair - Do not fail when getting ltvs - Simplied get_ltvs --- contracts/suilend/sources/lending_market.move | 11 +- contracts/suilend/sources/obligation.move | 217 ++++++++---------- contracts/suilend/sources/reserve_config.move | 23 +- contracts/suilend/tests/obligation_tests.move | 6 +- 4 files changed, 116 insertions(+), 141 deletions(-) diff --git a/contracts/suilend/sources/lending_market.move b/contracts/suilend/sources/lending_market.move index a81cac2..f1f877a 100644 --- a/contracts/suilend/sources/lending_market.move +++ b/contracts/suilend/sources/lending_market.move @@ -378,10 +378,11 @@ module suilend::lending_market { coin::from_balance(receive_balance, ctx) } - /// Set emode for obligation - T is the deposit coin type - public fun set_emode

( + /// Set and unset emode for obligation - T is the deposit coin type + public fun toggle_emode

( lending_market: &mut LendingMarket

, obligation_owner_cap: &ObligationOwnerCap

, + turn_on: bool, clock: &Clock ) { assert!(lending_market.version == CURRENT_VERSION, EIncorrectVersion); @@ -391,7 +392,11 @@ module suilend::lending_market { obligation_owner_cap.obligation_id ); - obligation::set_emode( + if (obligation.is_emode() == turn_on) { + return; + }; + + obligation::toggle_emode( obligation, &mut lending_market.reserves, clock, diff --git a/contracts/suilend/sources/obligation.move b/contracts/suilend/sources/obligation.move index 7de0321..65f0f36 100644 --- a/contracts/suilend/sources/obligation.move +++ b/contracts/suilend/sources/obligation.move @@ -169,18 +169,22 @@ module suilend::obligation { } } - public(package) fun set_emode

( + public(package) fun toggle_emode

( obligation: &mut Obligation

, reserves: &mut vector>, clock: &Clock, ) { assert!(!is_emode(obligation), EEModeAlreadySet); - assert!(vector::length(&obligation.borrows) <= 1, EEModeNotValidWithCrossMargin); - assert!(vector::length(&obligation.deposits) <= 1, EEModeNotValidWithCrossMargin); - refresh(obligation, reserves, clock); + if (is_emode(obligation)) { + df::remove(&mut obligation.id, EModeFlag {}); + } else { + assert!(vector::length(&obligation.borrows) <= 1, EEModeNotValidWithCrossMargin); + assert!(vector::length(&obligation.deposits) <= 1, EEModeNotValidWithCrossMargin); + df::add(&mut obligation.id, EModeFlag {}, true); + }; - df::add(&mut obligation.id, EModeFlag {}, true); + refresh(obligation, reserves, clock); } /// update the obligation's borrowed amounts and health values. this is @@ -197,7 +201,6 @@ module suilend::obligation { let is_emode = is_emode(obligation); if (is_emode) { obligation.refresh_emode(reserves, is_emode, clock) - } else { while (i < vector::length(&obligation.deposits)) { let deposit = vector::borrow_mut(&mut obligation.deposits, i); @@ -299,117 +302,90 @@ module suilend::obligation { is_emode: bool, clock: &Clock ) { - let mut i = 0; - let mut deposited_value_usd = decimal::from(0); - let mut allowed_borrow_value_usd = decimal::from(0); - let mut unhealthy_borrow_value_usd = decimal::from(0); + // Deposit - while (i < vector::length(&obligation.deposits)) { - let deposit = vector::borrow_mut(&mut obligation.deposits, i); + if (obligation.deposits.length() == 0) { + return + }; - let deposit_reserve = vector::borrow_mut(reserves, deposit.reserve_array_index); + assert!(vector::length(&obligation.deposits) == 1, 0); - reserve::compound_interest(deposit_reserve, clock); - reserve::assert_price_is_fresh(deposit_reserve, clock); + let deposit = obligation.deposits.borrow_mut(0); + let deposit_reserve = vector::borrow_mut(reserves, deposit.reserve_array_index); - 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); + reserve::compound_interest(deposit_reserve, clock); + reserve::assert_price_is_fresh(deposit_reserve, clock); - let (open_ltv, close_ltv) = get_ltvs( - obligation, - deposit_reserve, - is_emode, - ); + 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 + ); - allowed_borrow_value_usd = add( - allowed_borrow_value_usd, - mul( - market_value_lower_bound, - open_ltv, - ), - ); + deposit.market_value = market_value; + obligation.deposited_value_usd = market_value; - unhealthy_borrow_value_usd = add( - unhealthy_borrow_value_usd, - mul( - market_value, - close_ltv, - ), - ); + let (open_ltv, close_ltv, is_emode_ltvs) = get_ltvs( + obligation, + deposit_reserve, + is_emode, + ); - i = i + 1; - }; + obligation.allowed_borrow_value_usd = mul( + market_value_lower_bound, + open_ltv, + ); - 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; + obligation.unhealthy_borrow_value_usd = mul( + market_value, + close_ltv, + ); - let mut i = 0; - let mut unweighted_borrowed_value_usd = decimal::from(0); - let mut weighted_borrowed_value_usd = decimal::from(0); - let mut weighted_borrowed_value_upper_bound_usd = decimal::from(0); - let mut borrowing_isolated_asset = false; + // Borrow - while (i < vector::length(&obligation.borrows)) { - let borrow = vector::borrow_mut(&mut obligation.borrows, i); + if (obligation.borrows.length() == 0) { + return + }; - 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); + assert!(vector::length(&obligation.borrows) == 1, 0); - compound_debt(borrow, borrow_reserve); + let borrow = obligation.borrows.borrow_mut(0); - 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 - ); + 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); - borrow.market_value = market_value; - unweighted_borrowed_value_usd = add(unweighted_borrowed_value_usd, market_value); + compound_debt(borrow, borrow_reserve); - let borrow_weight = if (is_emode) { - decimal::from(1) - } else { - borrow_weight(config(borrow_reserve)) - }; - - weighted_borrowed_value_usd = add( - weighted_borrowed_value_usd, - mul( - market_value, - borrow_weight - ) - ); - weighted_borrowed_value_upper_bound_usd = add( - weighted_borrowed_value_upper_bound_usd, - mul( - market_value_upper_bound, - borrow_weight - ) - ); + 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 + ); - if (isolated(config(borrow_reserve))) { - borrowing_isolated_asset = true; - }; + borrow.market_value = market_value; + obligation.unweighted_borrowed_value_usd = market_value; - i = i + 1; + let borrow_weight = if (is_emode_ltvs) { + decimal::from(1) + } else { + borrow_weight(config(borrow_reserve)) }; - 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.weighted_borrowed_value_usd = mul( + market_value, + borrow_weight + ); - obligation.borrowing_isolated_asset = borrowing_isolated_asset; + obligation.weighted_borrowed_value_upper_bound_usd = mul( + market_value_upper_bound, + borrow_weight + ); + + obligation.borrowing_isolated_asset = isolated(config(borrow_reserve)); } /// Process a deposit action @@ -1375,41 +1351,34 @@ module suilend::obligation { obligation: &Obligation

, deposit_reserve: &Reserve

, is_emode: bool, - ): (Decimal, Decimal) { - if (is_emode) { - // check_normal_ltv_against_emode_ltvs - let borrow_reserve_index = get_single_borrow_array_reserve_if_any(obligation); + ): (Decimal, Decimal, bool) { + if (!is_emode) { + // When obligation normal mode + return (open_ltv(config(deposit_reserve)), close_ltv(config(deposit_reserve)), false) + }; - assert!( - reserve_config::has_emode_config(config(deposit_reserve)), - ENoEmodeConfigForGivenDepositReserve, - ); + let borrow_reserve_index = get_single_borrow_array_reserve_if_any(obligation); + if (borrow_reserve_index.is_none()) { + // When obligation is emode but there is no borrows + return (open_ltv(config(deposit_reserve)), close_ltv(config(deposit_reserve)), false) + }; - if (option::is_none(&borrow_reserve_index)) { - // When obligation is emode but there is no borrows - (open_ltv(config(deposit_reserve)), close_ltv(config(deposit_reserve))) - } else { - // When obligation is emode and there is a borrow - let emode_entry = reserve_config::try_get_emode_entry( - config(deposit_reserve), - option::borrow(&borrow_reserve_index) - ); + // When obligation is emode and there is a borrow + let emode_entry = reserve_config::try_get_emode_entry( + config(deposit_reserve), + option::borrow(&borrow_reserve_index) + ); - if (option::is_some(&emode_entry)) { - // When obligation is emode and there is a borrow AND there is a matching emode config - (open_ltv_emode(option::borrow(&emode_entry)), close_ltv_emode(option::borrow(&emode_entry))) - } else { - // When obligation is emode and there is a borrow BUT there is no matching emode config - (open_ltv(config(deposit_reserve)), close_ltv(config(deposit_reserve))) - } - } + if (option::is_some(&emode_entry)) { + // When obligation is emode and there is a borrow AND there is a matching emode config + (open_ltv_emode(option::borrow(&emode_entry)), close_ltv_emode(option::borrow(&emode_entry)), true) } else { - // When obligation normal mode - (open_ltv(config(deposit_reserve)), close_ltv(config(deposit_reserve))) + // When obligation is emode and there is a borrow BUT there is no matching emode config + (open_ltv(config(deposit_reserve)), close_ltv(config(deposit_reserve)), false) } } - fun is_emode

(obligation: &Obligation

): bool { + public(package) fun is_emode

(obligation: &Obligation

): bool { df::exists_(&obligation.id, EModeFlag {}) } diff --git a/contracts/suilend/sources/reserve_config.move b/contracts/suilend/sources/reserve_config.move index 189e462..5cb39ff 100644 --- a/contracts/suilend/sources/reserve_config.move +++ b/contracts/suilend/sources/reserve_config.move @@ -247,8 +247,8 @@ module suilend::reserve_config { while (vector::length(&keys) > 0) { let emode = vec_map::get(emode_ltvs, &vector::pop_back(&mut keys)); - assert!(config.open_ltv_pct < emode.open_ltv_pct, ENormalOpenLtvBetterThanEModeLtvs); - assert!(config.close_ltv_pct < emode.close_ltv_pct, ENormalCloseLtvBetterThanEModeLtvs); + assert!(config.open_ltv_pct <= emode.open_ltv_pct, ENormalOpenLtvBetterThanEModeLtvs); + assert!(config.close_ltv_pct <= emode.close_ltv_pct, ENormalCloseLtvBetterThanEModeLtvs); }; }; } @@ -575,15 +575,12 @@ module suilend::reserve_config { reserve_config: &ReserveConfig, reserve_array_index: &u64, ): bool { - let emode_config = get_emode_config(reserve_config); - vec_map::contains(emode_config, reserve_array_index) - } + if (!bag::contains(&reserve_config.additional_fields, EModeKey {})) { + return false + }; - public(package) fun get_emode_config( - reserve_config: &ReserveConfig, - ): &VecMap { - assert!(bag::contains(&reserve_config.additional_fields, EModeKey {}), ENoEModeConfigForDepositReserve); - bag::borrow(&reserve_config.additional_fields, EModeKey {}) + let emode_config: &VecMap = bag::borrow(&reserve_config.additional_fields, EModeKey {}); + vec_map::contains(emode_config, reserve_array_index) } public(package) fun open_ltv_emode( @@ -602,7 +599,11 @@ module suilend::reserve_config { reserve_config: &ReserveConfig, reserve_array_index: &u64, ): Option { - let emode_config = get_emode_config(reserve_config); + if (!bag::contains(&reserve_config.additional_fields, EModeKey {})) { + return option::none() + }; + + let emode_config = bag::borrow(&reserve_config.additional_fields, EModeKey {}); let has_pair = vec_map::contains(emode_config, reserve_array_index); if (has_pair) { diff --git a/contracts/suilend/tests/obligation_tests.move b/contracts/suilend/tests/obligation_tests.move index 9922d2a..8a2d5df 100644 --- a/contracts/suilend/tests/obligation_tests.move +++ b/contracts/suilend/tests/obligation_tests.move @@ -2306,8 +2306,8 @@ module suilend::obligation_tests { let new_config = builder.build(test_scenario::ctx(&mut scenario)); sui_reserve.update_reserve_config(new_config); - obligation.set_emode(&mut reserves, &clock); - obligation.set_emode(&mut reserves, &clock); + obligation.toggle_emode(&mut reserves, &clock); + obligation.toggle_emode(&mut reserves, &clock); sui::test_utils::destroy(lending_market_id); sui::test_utils::destroy(reserves); @@ -2360,7 +2360,7 @@ module suilend::obligation_tests { borrow(&mut obligation, get_reserve_mut(&mut reserves), &clock, 12_500_000); borrow(&mut obligation, get_reserve_mut(&mut reserves), &clock, 12_500_000); - obligation.set_emode(&mut reserves, &clock); + obligation.toggle_emode(&mut reserves, &clock); refresh(&mut obligation, &mut reserves, &clock); From 09bb04de3f87a6ccb7b1852d0bb3a8afa6a2caee Mon Sep 17 00:00:00 2001 From: 0xxgen1 <0xxgen@solend.fi> Date: Fri, 8 Nov 2024 12:52:47 +0000 Subject: [PATCH 5/7] toggle emode tests --- contracts/suilend/sources/lending_market.move | 2 +- contracts/suilend/sources/obligation.move | 2 - .../suilend/tests/lending_market_tests.move | 79 +++++++++++++++++++ contracts/suilend/tests/obligation_tests.move | 5 +- 4 files changed, 83 insertions(+), 5 deletions(-) diff --git a/contracts/suilend/sources/lending_market.move b/contracts/suilend/sources/lending_market.move index f1f877a..10adaf8 100644 --- a/contracts/suilend/sources/lending_market.move +++ b/contracts/suilend/sources/lending_market.move @@ -393,7 +393,7 @@ module suilend::lending_market { ); if (obligation.is_emode() == turn_on) { - return; + return }; obligation::toggle_emode( diff --git a/contracts/suilend/sources/obligation.move b/contracts/suilend/sources/obligation.move index 65f0f36..758fcd6 100644 --- a/contracts/suilend/sources/obligation.move +++ b/contracts/suilend/sources/obligation.move @@ -174,8 +174,6 @@ module suilend::obligation { reserves: &mut vector>, clock: &Clock, ) { - assert!(!is_emode(obligation), EEModeAlreadySet); - if (is_emode(obligation)) { df::remove(&mut obligation.id, EModeFlag {}); } else { diff --git a/contracts/suilend/tests/lending_market_tests.move b/contracts/suilend/tests/lending_market_tests.move index 44a3587..89d06f8 100644 --- a/contracts/suilend/tests/lending_market_tests.move +++ b/contracts/suilend/tests/lending_market_tests.move @@ -1621,4 +1621,83 @@ module suilend::lending_market_tests { test_utils::destroy(type_to_index); test_scenario::end(scenario); } + + #[test] + public fun test_toggle_emode_on_and_off() { + use sui::test_utils::{Self}; + use suilend::test_usdc::{TEST_USDC}; + use suilend::test_sui::{TEST_SUI}; + use suilend::mock_pyth::{Self}; + use suilend::reserve_config::{Self, default_reserve_config}; + + use std::type_name::{Self}; + + let owner = @0x26; + let mut scenario = test_scenario::begin(owner); + let State { mut clock, owner_cap, mut lending_market, mut prices, type_to_index } = setup({ + let mut bag = bag::new(test_scenario::ctx(&mut scenario)); + bag::add( + &mut bag, + type_name::get(), + ReserveArgs { + config: { + let config = default_reserve_config(); + let mut builder = reserve_config::from(&config, test_scenario::ctx(&mut scenario)); + reserve_config::set_open_ltv_pct(&mut builder, 50); + reserve_config::set_close_ltv_pct(&mut builder, 50); + reserve_config::set_max_close_ltv_pct(&mut builder, 50); + sui::test_utils::destroy(config); + + reserve_config::build(builder, test_scenario::ctx(&mut scenario)) + }, + initial_deposit: 100 * 1_000_000 + } + ); + bag::add( + &mut bag, + type_name::get(), + ReserveArgs { + config: { + let config = reserve_config::default_reserve_config(); + let mut builder = reserve_config::from( + &config, + test_scenario::ctx(&mut scenario) + ); + + let emode_ltvs = reserve_config::create_emode_entry( + 0, // USDC array index + 40, + 60, + ); + + builder.set_emode_ltv_for_borrow(emode_ltvs); + + test_utils::destroy(config); + + reserve_config::set_borrow_fee_bps(&mut builder, 10); + reserve_config::build(builder, test_scenario::ctx(&mut scenario)) + }, + initial_deposit: 100 * 1_000_000_000 + } + ); + + bag + }, &mut scenario); + + let obligation_owner_cap = lending_market::create_obligation( + &mut lending_market, + test_scenario::ctx(&mut scenario) + ); + + lending_market.toggle_emode(&obligation_owner_cap, true, &clock); + lending_market.toggle_emode(&obligation_owner_cap, false, &clock); + + test_utils::destroy(owner_cap); + test_utils::destroy(obligation_owner_cap); + test_utils::destroy(lending_market); + test_utils::destroy(clock); + test_utils::destroy(prices); + test_utils::destroy(type_to_index); + test_scenario::end(scenario); + } } \ No newline at end of file diff --git a/contracts/suilend/tests/obligation_tests.move b/contracts/suilend/tests/obligation_tests.move index 8a2d5df..de0559c 100644 --- a/contracts/suilend/tests/obligation_tests.move +++ b/contracts/suilend/tests/obligation_tests.move @@ -2266,8 +2266,7 @@ module suilend::obligation_tests { } #[test] - #[expected_failure(abort_code = suilend::obligation::EEModeAlreadySet)] - public fun test_emode_already_set() { + public fun test_set_emod_on_and_off() { use sui::test_scenario::{Self}; let owner = @0x26; @@ -2307,7 +2306,9 @@ module suilend::obligation_tests { sui_reserve.update_reserve_config(new_config); obligation.toggle_emode(&mut reserves, &clock); + assert!(obligation.is_emode(), 0); obligation.toggle_emode(&mut reserves, &clock); + assert!(!obligation.is_emode(), 0); sui::test_utils::destroy(lending_market_id); sui::test_utils::destroy(reserves); From 7d592c184a172488e1fe3aa1fc0187e13e9abb68 Mon Sep 17 00:00:00 2001 From: 0xxgen1 <0xxgen@solend.fi> Date: Fri, 8 Nov 2024 17:34:11 +0000 Subject: [PATCH 6/7] Nit: if-else statement --- contracts/suilend/sources/obligation.move | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/suilend/sources/obligation.move b/contracts/suilend/sources/obligation.move index 758fcd6..67356b5 100644 --- a/contracts/suilend/sources/obligation.move +++ b/contracts/suilend/sources/obligation.move @@ -198,8 +198,10 @@ module suilend::obligation { let mut unhealthy_borrow_value_usd = decimal::from(0); let is_emode = is_emode(obligation); if (is_emode) { - obligation.refresh_emode(reserves, is_emode, clock) - } else { + obligation.refresh_emode(reserves, is_emode, clock); + return + }; + while (i < vector::length(&obligation.deposits)) { let deposit = vector::borrow_mut(&mut obligation.deposits, i); @@ -291,7 +293,6 @@ module suilend::obligation { obligation.weighted_borrowed_value_upper_bound_usd = weighted_borrowed_value_upper_bound_usd; obligation.borrowing_isolated_asset = borrowing_isolated_asset; - }; } fun refresh_emode

( From 881f3507d002a810d421ccfd474f85f209c9394b Mon Sep 17 00:00:00 2001 From: 0xxgen1 <0xxgen@solend.fi> Date: Fri, 8 Nov 2024 22:23:06 +0000 Subject: [PATCH 7/7] Add back toggle emode with reserve arrays --- contracts/suilend/sources/lending_market.move | 31 ++++++++++--- contracts/suilend/sources/obligation.move | 43 ++++++++++++++++--- .../suilend/tests/lending_market_tests.move | 4 +- contracts/suilend/tests/obligation_tests.move | 10 ++--- 4 files changed, 68 insertions(+), 20 deletions(-) diff --git a/contracts/suilend/sources/lending_market.move b/contracts/suilend/sources/lending_market.move index 10adaf8..4cfb8f9 100644 --- a/contracts/suilend/sources/lending_market.move +++ b/contracts/suilend/sources/lending_market.move @@ -379,24 +379,43 @@ module suilend::lending_market { } /// Set and unset emode for obligation - T is the deposit coin type - public fun toggle_emode

( + public fun toggle_emode_on( lending_market: &mut LendingMarket

, obligation_owner_cap: &ObligationOwnerCap

, - turn_on: bool, clock: &Clock ) { assert!(lending_market.version == CURRENT_VERSION, EIncorrectVersion); + let deposit_reserve_array_index = lending_market.reserve_array_index(); + let borrow_reserve_array_index = lending_market.reserve_array_index(); + let obligation = object_table::borrow_mut( &mut lending_market.obligations, obligation_owner_cap.obligation_id ); - if (obligation.is_emode() == turn_on) { - return - }; + obligation::toggle_emode_on( + obligation, + &mut lending_market.reserves, + deposit_reserve_array_index, + borrow_reserve_array_index, + clock, + ); + } + + public fun toggle_emode_off

( + lending_market: &mut LendingMarket

, + obligation_owner_cap: &ObligationOwnerCap

, + clock: &Clock + ) { + assert!(lending_market.version == CURRENT_VERSION, EIncorrectVersion); + + let obligation = object_table::borrow_mut( + &mut lending_market.obligations, + obligation_owner_cap.obligation_id + ); - obligation::toggle_emode( + obligation::toggle_emode_off( obligation, &mut lending_market.reserves, clock, diff --git a/contracts/suilend/sources/obligation.move b/contracts/suilend/sources/obligation.move index 67356b5..f9083ab 100644 --- a/contracts/suilend/sources/obligation.move +++ b/contracts/suilend/sources/obligation.move @@ -46,6 +46,10 @@ module suilend::obligation { // === Dynamic Field Keys === public struct EModeFlag has store, copy, drop {} + public struct EModeReserveIndices has store, copy, drop { + deposit_reserve_array_index: u64, + borrow_reserve_array_index: u64, + } // === public structs === public struct Obligation has key, store { @@ -169,19 +173,44 @@ module suilend::obligation { } } - public(package) fun toggle_emode

( + public(package) fun toggle_emode_on

( obligation: &mut Obligation

, reserves: &mut vector>, + deposit_reserve_array_index: u64, + borrow_reserve_array_index: u64, clock: &Clock, ) { - if (is_emode(obligation)) { - df::remove(&mut obligation.id, EModeFlag {}); - } else { - assert!(vector::length(&obligation.borrows) <= 1, EEModeNotValidWithCrossMargin); - assert!(vector::length(&obligation.deposits) <= 1, EEModeNotValidWithCrossMargin); - df::add(&mut obligation.id, EModeFlag {}, true); + assert!(!obligation.is_emode(), EEModeAlreadySet); + + assert!(vector::length(&obligation.borrows) <= 1, EEModeNotValidWithCrossMargin); + assert!(vector::length(&obligation.deposits) <= 1, EEModeNotValidWithCrossMargin); + + if (vector::length(&obligation.borrows) == 1) { + assert!( + obligation.borrows.borrow(0).reserve_array_index == borrow_reserve_array_index, + EInvalidEModeBorrow + ); + }; + + if (vector::length(&obligation.deposits) == 1) { + assert!( + obligation.deposits.borrow(0).reserve_array_index == deposit_reserve_array_index, + EInvalidEModeDeposit + ); }; + df::add(&mut obligation.id, EModeFlag {}, EModeReserveIndices { deposit_reserve_array_index, borrow_reserve_array_index }); + + refresh(obligation, reserves, clock); + } + + public(package) fun toggle_emode_off

( + obligation: &mut Obligation

, + reserves: &mut vector>, + clock: &Clock, + ) { + df::remove(&mut obligation.id, EModeFlag {}); + refresh(obligation, reserves, clock); } diff --git a/contracts/suilend/tests/lending_market_tests.move b/contracts/suilend/tests/lending_market_tests.move index 89d06f8..9e9dbb5 100644 --- a/contracts/suilend/tests/lending_market_tests.move +++ b/contracts/suilend/tests/lending_market_tests.move @@ -1689,8 +1689,8 @@ module suilend::lending_market_tests { test_scenario::ctx(&mut scenario) ); - lending_market.toggle_emode(&obligation_owner_cap, true, &clock); - lending_market.toggle_emode(&obligation_owner_cap, false, &clock); + lending_market.toggle_emode_on(&obligation_owner_cap, &clock); + lending_market.toggle_emode_off(&obligation_owner_cap, &clock); test_utils::destroy(owner_cap); test_utils::destroy(obligation_owner_cap); diff --git a/contracts/suilend/tests/obligation_tests.move b/contracts/suilend/tests/obligation_tests.move index de0559c..92b437b 100644 --- a/contracts/suilend/tests/obligation_tests.move +++ b/contracts/suilend/tests/obligation_tests.move @@ -2266,7 +2266,7 @@ module suilend::obligation_tests { } #[test] - public fun test_set_emod_on_and_off() { + public fun test_set_emode_on_and_off() { use sui::test_scenario::{Self}; let owner = @0x26; @@ -2281,7 +2281,7 @@ module suilend::obligation_tests { reserve::update_price_for_testing( get_reserve_mut(&mut reserves), &clock, - decimal::from(1), + decimal::from(1), decimal::from(2) ); reserve::update_price_for_testing( @@ -2305,9 +2305,9 @@ module suilend::obligation_tests { let new_config = builder.build(test_scenario::ctx(&mut scenario)); sui_reserve.update_reserve_config(new_config); - obligation.toggle_emode(&mut reserves, &clock); + obligation.toggle_emode_on(&mut reserves, 0, 1, &clock); assert!(obligation.is_emode(), 0); - obligation.toggle_emode(&mut reserves, &clock); + obligation.toggle_emode_off(&mut reserves, &clock); assert!(!obligation.is_emode(), 0); sui::test_utils::destroy(lending_market_id); @@ -2361,7 +2361,7 @@ module suilend::obligation_tests { borrow(&mut obligation, get_reserve_mut(&mut reserves), &clock, 12_500_000); borrow(&mut obligation, get_reserve_mut(&mut reserves), &clock, 12_500_000); - obligation.toggle_emode(&mut reserves, &clock); + obligation.toggle_emode_on(&mut reserves, 0, 1, &clock); refresh(&mut obligation, &mut reserves, &clock);