From 6e3cbe543048c96259147914da9504fd5f09d313 Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Wed, 27 Nov 2024 16:53:03 -0500 Subject: [PATCH] optionally unstake when claiming sui spread fees --- contracts/suilend/sources/lending_market.move | 7 +- contracts/suilend/sources/reserve.move | 22 ++- .../suilend/tests/lending_market_tests.move | 36 +++-- contracts/suilend/tests/reserve_tests.move | 146 +++++++++++++++++- 4 files changed, 183 insertions(+), 28 deletions(-) diff --git a/contracts/suilend/sources/lending_market.move b/contracts/suilend/sources/lending_market.move index 1dfacd0..bb96f62 100644 --- a/contracts/suilend/sources/lending_market.move +++ b/contracts/suilend/sources/lending_market.move @@ -809,7 +809,7 @@ module suilend::lending_market { return }; - reserve::unstake_sui_from_staker

(reserve, liquidity_request, system_state, ctx); + reserve::unstake_sui_from_staker(reserve, liquidity_request, system_state, ctx); } // === Public-View Functions === @@ -1124,14 +1124,15 @@ module suilend::lending_market { entry fun claim_fees( lending_market: &mut LendingMarket

, reserve_array_index: u64, - ctx: &mut TxContext, + system_state: &mut SuiSystemState, + ctx: &mut TxContext ) { 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); - let (mut ctoken_fees, mut fees) = reserve::claim_fees(reserve); + let (mut ctoken_fees, mut fees) = reserve::claim_fees(reserve, system_state, ctx); let total_ctoken_fees = balance::value(&ctoken_fees); let total_fees = balance::value(&fees); diff --git a/contracts/suilend/sources/reserve.move b/contracts/suilend/sources/reserve.move index 0147c91..421e81d 100644 --- a/contracts/suilend/sources/reserve.move +++ b/contracts/suilend/sources/reserve.move @@ -622,7 +622,11 @@ module suilend::reserve { }); } - public(package) fun claim_fees(reserve: &mut Reserve

): (Balance>, Balance) { + public(package) fun claim_fees( + reserve: &mut Reserve

, + system_state: &mut SuiSystemState, + ctx: &mut TxContext + ): (Balance>, Balance) { let balances: &mut Balances = dynamic_field::borrow_mut(&mut reserve.id, BalanceKey {}); let mut fees = balance::withdraw_all(&mut balances.fees); let ctoken_fees = balance::withdraw_all(&mut balances.ctoken_fees); @@ -634,7 +638,15 @@ module suilend::reserve { decimal::from(reserve.available_amount - MIN_AVAILABLE_AMOUNT) )); - let spread_fees = balance::split(&mut balances.available_amount, claimable_spread_fees); + let spread_fees = { + let liquidity_request = LiquidityRequest { amount: claimable_spread_fees, fee: 0 }; + + if (type_name::get() == type_name::get()) { + unstake_sui_from_staker(reserve, &liquidity_request, system_state, ctx); + }; + + fulfill_liquidity_request(reserve, liquidity_request) + }; reserve.unclaimed_spread_fees = sub( reserve.unclaimed_spread_fees, @@ -783,13 +795,13 @@ module suilend::reserve { }; } - public(package) fun unstake_sui_from_staker

( + public(package) fun unstake_sui_from_staker( reserve: &mut Reserve

, - liquidity_request: &LiquidityRequest, + liquidity_request: &LiquidityRequest, system_state: &mut SuiSystemState, ctx: &mut TxContext ) { - assert!(reserve.coin_type == type_name::get(), EWrongType); + assert!(reserve.coin_type == type_name::get() && type_name::get() == type_name::get(), EWrongType); if (!dynamic_field::exists_(&reserve.id, StakerKey {})) { return }; diff --git a/contracts/suilend/tests/lending_market_tests.move b/contracts/suilend/tests/lending_market_tests.move index aed6fc5..f728407 100644 --- a/contracts/suilend/tests/lending_market_tests.move +++ b/contracts/suilend/tests/lending_market_tests.move @@ -440,6 +440,7 @@ module suilend::lending_market_tests { let owner = @0x26; let mut scenario = test_scenario::begin(owner); + setup_sui_system(&mut scenario); 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( @@ -601,17 +602,13 @@ module suilend::lending_market_tests { let sui_reserve = lending_market::reserve(&lending_market); assert!(reserve::borrowed_amount(sui_reserve) == decimal::from(0), 0); - let obligation = lending_market::obligation( - &lending_market, - lending_market::obligation_id(&obligation_owner_cap), - ); - assert!( - obligation::borrowed_amount(obligation) == decimal::from(0), - 0, - ); + let obligation = lending_market::obligation(&lending_market, lending_market::obligation_id(&obligation_owner_cap)); + assert!(obligation::borrowed_amount(obligation) == decimal::from(0), 0); test_scenario::next_tx(&mut scenario, owner); + let mut system_state = test_scenario::take_shared(&mut scenario); + lending_market::set_fee_receivers( &owner_cap, &mut lending_market, @@ -622,8 +619,17 @@ module suilend::lending_market_tests { lending_market::claim_fees( &mut lending_market, *bag::borrow(&type_to_index, type_name::get()), + &mut system_state, test_scenario::ctx(&mut scenario), ); + lending_market::claim_fees( + &mut lending_market, + *bag::borrow(&type_to_index, type_name::get()), + &mut system_state, + test_scenario::ctx(&mut scenario) + ); + + test_scenario::return_shared(system_state); test_scenario::next_tx(&mut scenario, owner); @@ -793,6 +799,8 @@ module suilend::lending_market_tests { let owner = @0x26; let mut scenario = test_scenario::begin(owner); + setup_sui_system(&mut scenario); + 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( @@ -979,13 +987,16 @@ module suilend::lending_market_tests { // claim fees test_scenario::next_tx(&mut scenario, owner); + let mut system_state = test_scenario::take_shared(&mut scenario); lending_market::claim_fees( &mut lending_market, *bag::borrow(&type_to_index, type_name::get()), - test_scenario::ctx(&mut scenario), + &mut system_state, + test_scenario::ctx(&mut scenario) ); - + test_scenario::return_shared(system_state); test_scenario::next_tx(&mut scenario, owner); + let ctoken_fees: Coin> = test_scenario::take_from_address( &scenario, lending_market::fee_receiver(&lending_market), @@ -2187,7 +2198,6 @@ module suilend::lending_market_tests { &mut system_state, test_scenario::ctx(&mut scenario), ); - test_scenario::return_shared(system_state); let sui_reserve = lending_market::reserve(&lending_market); let staker = reserve::staker(sui_reserve); @@ -2199,9 +2209,11 @@ module suilend::lending_market_tests { lending_market::claim_fees( &mut lending_market, *bag::borrow(&type_to_index, type_name::get()), - test_scenario::ctx(&mut scenario), + &mut system_state, + test_scenario::ctx(&mut scenario) ); + test_scenario::return_shared(system_state); test_scenario::next_tx(&mut scenario, owner); let fees: Coin = test_scenario::take_from_address( diff --git a/contracts/suilend/tests/reserve_tests.move b/contracts/suilend/tests/reserve_tests.move index 1e1da62..2a51d64 100644 --- a/contracts/suilend/tests/reserve_tests.move +++ b/contracts/suilend/tests/reserve_tests.move @@ -1,8 +1,24 @@ module suilend::reserve_tests { - use sui::balance; - use sui::clock; - use sui::test_scenario; - use suilend::decimal::{Self, add, sub}; + use sui::sui::{SUI}; + use sui::balance::{Self, Balance, Supply}; + use sprungsui::sprungsui::SPRUNGSUI; + use sui::coin::{Self}; + use suilend::decimal::{Decimal, Self, add, sub, mul, div, eq, floor, pow, le, ceil, min, max, saturating_sub}; + use pyth::price_identifier::{PriceIdentifier}; + use suilend::reserve_config::{ + Self, + ReserveConfig, + calculate_apr, + calculate_supply_apr, + deposit_limit, + deposit_limit_usd, + borrow_limit, + borrow_limit_usd, + borrow_fee, + protocol_liquidation_fee, + spread_fee, + liquidation_bonus + }; use suilend::reserve::{ Self, create_for_testing, @@ -14,7 +30,10 @@ module suilend::reserve_tests { repay_liquidity, Balances }; - use suilend::reserve_config; + use sui::clock::{Self}; + use suilend::liquidity_mining::{Self, PoolRewardManager}; + use sui_system::sui_system::{SuiSystemState}; + use sui::test_scenario::{Self, Scenario}; #[test_only] public struct TEST_LM {} @@ -192,7 +211,7 @@ module suilend::reserve_tests { let owner = @0x26; let mut scenario = test_scenario::begin(owner); - + setup_sui_system(&mut scenario); let mut reserve = create_for_testing( { let config = default_reserve_config(); @@ -237,7 +256,10 @@ module suilend::reserve_tests { assert!(balance::value(balances.available_amount()) == available_amount_old - 404, 0); assert!(balance::value(balances.fees()) == 4, 0); - let (ctoken_fees, fees) = claim_fees(&mut reserve); + let mut system_state = test_scenario::take_shared(&mut scenario); + let (ctoken_fees, fees) = claim_fees(&mut reserve, &mut system_state, test_scenario::ctx(&mut scenario)); + test_scenario::return_shared(system_state); + assert!(balance::value(&fees) == 4, 0); assert!(balance::value(&ctoken_fees) == 0, 0); @@ -345,6 +367,7 @@ module suilend::reserve_tests { let owner = @0x26; let mut scenario = test_scenario::begin(owner); + setup_sui_system(&mut scenario); let mut clock = clock::create_for_testing(test_scenario::ctx(&mut scenario)); let mut reserve = create_for_testing( @@ -398,7 +421,9 @@ module suilend::reserve_tests { let old_available_amount = reserve.available_amount(); let old_unclaimed_spread_fees = reserve.unclaimed_spread_fees(); - let (ctoken_fees, fees) = claim_fees(&mut reserve); + let mut system_state = test_scenario::take_shared(&mut scenario); + let (ctoken_fees, fees) = claim_fees(&mut reserve, &mut system_state, test_scenario::ctx(&mut scenario)); + test_scenario::return_shared(system_state); // 0.5% interest a second with 50% take rate => 0.25% fee on 50 USDC = 0.125 USDC assert!(balance::value(&fees) == 125_000, 0); @@ -423,6 +448,111 @@ module suilend::reserve_tests { test_scenario::end(scenario); } + use sui_system::governance_test_utils::{ + advance_epoch_with_reward_amounts, + create_validator_for_testing, + create_sui_system_state_for_testing, + }; + + + const SUILEND_VALIDATOR: address = @0xce8e537664ba5d1d5a6a857b17bd142097138706281882be6805e17065ecde89; + + fun setup_sui_system(scenario: &mut Scenario) { + test_scenario::next_tx(scenario, SUILEND_VALIDATOR); + let validator = create_validator_for_testing(SUILEND_VALIDATOR, 100, test_scenario::ctx(scenario)); + create_sui_system_state_for_testing(vector[validator], 0, 0, test_scenario::ctx(scenario)); + + advance_epoch_with_reward_amounts(0, 0, scenario); + } + + #[test] + fun test_claim_fees_with_staker() { + use sui::test_scenario::{Self}; + use suilend::reserve_config::{default_reserve_config}; + + let owner = @0x26; + let mut scenario = test_scenario::begin(owner); + setup_sui_system(&mut scenario); + + let mut clock = clock::create_for_testing(test_scenario::ctx(&mut scenario)); + + let mut reserve = create_for_testing( + { + let config = default_reserve_config(); + let mut builder = reserve_config::from(&config, test_scenario::ctx(&mut scenario)); + reserve_config::set_spread_fee_bps(&mut builder, 5000); + reserve_config::set_interest_rate_utils(&mut builder, { + let mut v = vector::empty(); + vector::push_back(&mut v, 0); + vector::push_back(&mut v, 100); + v + }); + reserve_config::set_interest_rate_aprs(&mut builder, { + let mut v = vector::empty(); + vector::push_back(&mut v, 0); + vector::push_back(&mut v, 3153600000); + v + }); + + sui::test_utils::destroy(config); + + reserve_config::build(builder, test_scenario::ctx(&mut scenario)) + }, + 0, + 9, + decimal::from(1), + 0, + 0, + 0, + decimal::from(0), + decimal::from(1), + 0, + test_scenario::ctx(&mut scenario) + ); + + let ctokens = deposit_liquidity_and_mint_ctokens( + &mut reserve, + balance::create_for_testing(100 * 1_000_000_000) + ); + + let liquidity_request = borrow_liquidity(&mut reserve, 50 * 1_000_000_000); + let tokens = reserve::fulfill_liquidity_request(&mut reserve, liquidity_request); + + clock::set_for_testing(&mut clock, 1000); + compound_interest(&mut reserve, &clock); + + let old_available_amount = reserve.available_amount(); + let old_unclaimed_spread_fees = reserve.unclaimed_spread_fees(); + + let mut system_state = test_scenario::take_shared(&scenario); + let treasury_cap = coin::create_treasury_cap_for_testing(scenario.ctx()); + reserve::init_staker(&mut reserve, treasury_cap, test_scenario::ctx(&mut scenario)); + reserve::rebalance_staker(&mut reserve, &mut system_state, test_scenario::ctx(&mut scenario)); + + let (ctoken_fees, fees) = claim_fees(&mut reserve, &mut system_state, test_scenario::ctx(&mut scenario)); + + // 0.5% interest a second with 50% take rate => 0.25% fee on 50 SUI = 0.125 SUI + assert!(balance::value(&fees) == 125_000_000, 0); + assert!(balance::value(&ctoken_fees) == 0, 0); + + assert!(reserve.available_amount() == old_available_amount - 125_000_000, 0); + assert!(reserve.unclaimed_spread_fees() == sub(old_unclaimed_spread_fees, decimal::from(125_000_000)), 0); + + let balances: &Balances = reserve::balances(&reserve); + assert!(balance::value(balances.available_amount()) == 0, 0); // all the sui has been staked + + test_scenario::return_shared(system_state); + + sui::test_utils::destroy(clock); + sui::test_utils::destroy(ctoken_fees); + sui::test_utils::destroy(fees); + sui::test_utils::destroy(reserve); + sui::test_utils::destroy(tokens); + sui::test_utils::destroy(ctokens); + + test_scenario::end(scenario); + } + #[test] fun test_repay_happy() { use suilend::test_usdc::{TEST_USDC};