Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feature/add refresh obligation #56

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions contracts/suilend/sources/lending_market.move
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,23 @@ module suilend::lending_market {
object_table::borrow(&lending_market.obligations, obligation_id)
}

/// Refresh the obligation's state by updating prices and checking for stale oracles
public fun refresh_obligation<P>(
lending_market: &mut LendingMarket<P>,
obligation_owner_cap: &ObligationOwnerCap<P>,
clock: &Clock,
) {
assert!(lending_market.version == CURRENT_VERSION, EIncorrectVersion);

let obligation = object_table::borrow_mut(
&mut lending_market.obligations,
obligation_owner_cap.obligation_id,
);

let exist_stale_oracles = obligation::refresh<P>(obligation, &mut lending_market.reserves, clock);
obligation::assert_no_stale_oracles(exist_stale_oracles);
}

public fun fee_receiver<P>(lending_market: &LendingMarket<P>): address {
lending_market.fee_receiver
}
Expand Down
270 changes: 269 additions & 1 deletion contracts/suilend/tests/lending_market_tests.move
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ module suilend::lending_market_tests {
LendingMarket
};
use suilend::mock_pyth::PriceState;
use suilend::obligation;
use suilend::obligation::{Self, unweighted_borrowed_value_usd, deposited_value_usd};
use suilend::rate_limiter;
use suilend::reserve::{Self, CToken};
use suilend::reserve_config::ReserveConfig;
Expand Down Expand Up @@ -2489,4 +2489,272 @@ module suilend::lending_market_tests {
test_utils::destroy(type_to_index);
test_scenario::end(scenario);
}

#[test]
public fun test_refresh_obligation() {
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};

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<TEST_USDC>(),
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<TEST_SUI>(),
ReserveArgs {
config: reserve_config::default_reserve_config(),
initial_deposit: 100 * 1_000_000_000,
},
);

bag
}, &mut scenario);

clock::set_for_testing(&mut clock, 1 * 1000);

// Set initial prices
mock_pyth::update_price<TEST_USDC>(&mut prices, 1, 0, &clock); // $1
mock_pyth::update_price<TEST_SUI>(&mut prices, 1, 1, &clock); // $10

// Create obligation and deposit collateral
let obligation_owner_cap = lending_market::create_obligation(
&mut lending_market,
test_scenario::ctx(&mut scenario),
);

let coins = coin::mint_for_testing<TEST_USDC>(
100 * 1_000_000,
test_scenario::ctx(&mut scenario),
);
let ctokens = lending_market::deposit_liquidity_and_mint_ctokens<LENDING_MARKET, TEST_USDC>(
&mut lending_market,
*bag::borrow(&type_to_index, type_name::get<TEST_USDC>()),
&clock,
coins,
test_scenario::ctx(&mut scenario),
);
lending_market::deposit_ctokens_into_obligation<LENDING_MARKET, TEST_USDC>(
&mut lending_market,
*bag::borrow(&type_to_index, type_name::get<TEST_USDC>()),
&obligation_owner_cap,
&clock,
ctokens,
test_scenario::ctx(&mut scenario),
);

// Take out a borrow
lending_market::refresh_reserve_price<LENDING_MARKET>(
&mut lending_market,
*bag::borrow(&type_to_index, type_name::get<TEST_USDC>()),
&clock,
mock_pyth::get_price_obj<TEST_USDC>(&prices),
);
lending_market::refresh_reserve_price<LENDING_MARKET>(
&mut lending_market,
*bag::borrow(&type_to_index, type_name::get<TEST_SUI>()),
&clock,
mock_pyth::get_price_obj<TEST_SUI>(&prices),
);

let sui = lending_market::borrow<LENDING_MARKET, TEST_SUI>(
&mut lending_market,
*bag::borrow(&type_to_index, type_name::get<TEST_SUI>()),
&obligation_owner_cap,
&clock,
2_500_000_000,
test_scenario::ctx(&mut scenario),
);

// Get initial obligation values
let obligation_id = lending_market::obligation_id(&obligation_owner_cap);
let obligation = lending_market::obligation<LENDING_MARKET>(&lending_market, obligation_id);
let initial_borrowed_value = unweighted_borrowed_value_usd<LENDING_MARKET>(obligation);
let initial_deposited_value = deposited_value_usd<LENDING_MARKET>(obligation);

// initial borrow should be 25. i.e. borrowing 2_500_000_000 (which is 2.5 SUI) and initial price is 10$
assert!(25 == decimal::floor(initial_borrowed_value), 0);

// initial deposit should be 100 USD (100 * 1_000_000 * 1$ price)
assert!(100 == decimal::floor(initial_deposited_value), 0);

// Update prices to simulate market changes
mock_pyth::update_price<TEST_SUI>(&mut prices, 2, 1, &clock); // Now $20 (doubled)
// refresh reserve with new prices
lending_market::refresh_reserve_price<LENDING_MARKET>(
&mut lending_market,
*bag::borrow(&type_to_index, type_name::get<TEST_SUI>()),
&clock,
mock_pyth::get_price_obj<TEST_SUI>(&prices),
);
// Refresh obligation with new prices
lending_market::refresh_obligation<LENDING_MARKET>(
&mut lending_market,
&obligation_owner_cap,
&clock,
);

// Get updated obligation values
let obligation = lending_market::obligation<LENDING_MARKET>(&lending_market, obligation_id);
let borrowed_value = unweighted_borrowed_value_usd<LENDING_MARKET>(obligation);
let deposited_value = deposited_value_usd<LENDING_MARKET>(obligation);

// price for SUI doubled, now borrow should be 50
assert!(50 == decimal::floor(borrowed_value), 0);

// price for USDC did not change, should still be 100
assert!(100 == decimal::floor(deposited_value), 0);

test_utils::destroy(sui);
test_utils::destroy(obligation_owner_cap);
test_utils::destroy(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);
}

#[test]
#[expected_failure(abort_code = suilend::obligation::EOraclesAreStale)]
public fun test_refresh_obligation_stale_oracle() {
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};

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<TEST_USDC>(),
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<TEST_SUI>(),
ReserveArgs {
config: reserve_config::default_reserve_config(),
initial_deposit: 100 * 1_000_000_000,
},
);

bag
}, &mut scenario);

clock::set_for_testing(&mut clock, 1 * 1000);

// Set initial prices
mock_pyth::update_price<TEST_USDC>(&mut prices, 1, 0, &clock); // $1
mock_pyth::update_price<TEST_SUI>(&mut prices, 1, 1, &clock); // $10

// Create obligation and deposit collateral
let obligation_owner_cap = lending_market::create_obligation(
&mut lending_market,
test_scenario::ctx(&mut scenario),
);

let coins = coin::mint_for_testing<TEST_USDC>(
100 * 1_000_000,
test_scenario::ctx(&mut scenario),
);
let ctokens = lending_market::deposit_liquidity_and_mint_ctokens<LENDING_MARKET, TEST_USDC>(
&mut lending_market,
*bag::borrow(&type_to_index, type_name::get<TEST_USDC>()),
&clock,
coins,
test_scenario::ctx(&mut scenario),
);
lending_market::deposit_ctokens_into_obligation<LENDING_MARKET, TEST_USDC>(
&mut lending_market,
*bag::borrow(&type_to_index, type_name::get<TEST_USDC>()),
&obligation_owner_cap,
&clock,
ctokens,
test_scenario::ctx(&mut scenario),
);

// Take out a borrow
lending_market::refresh_reserve_price<LENDING_MARKET>(
&mut lending_market,
*bag::borrow(&type_to_index, type_name::get<TEST_USDC>()),
&clock,
mock_pyth::get_price_obj<TEST_USDC>(&prices),
);
lending_market::refresh_reserve_price<LENDING_MARKET>(
&mut lending_market,
*bag::borrow(&type_to_index, type_name::get<TEST_SUI>()),
&clock,
mock_pyth::get_price_obj<TEST_SUI>(&prices),
);

let sui = lending_market::borrow<LENDING_MARKET, TEST_SUI>(
&mut lending_market,
*bag::borrow(&type_to_index, type_name::get<TEST_SUI>()),
&obligation_owner_cap,
&clock,
2_500_000_000,
test_scenario::ctx(&mut scenario),
);

// Advance clock to make oracles stale
clock::increment_for_testing(&mut clock, 100_000);

// This should fail with EOraclesAreStale
lending_market::refresh_obligation<LENDING_MARKET>(
&mut lending_market,
&obligation_owner_cap,
&clock,
);

test_utils::destroy(sui);
test_utils::destroy(obligation_owner_cap);
test_utils::destroy(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);
}
}