diff --git a/contracts/sprungsui/Move.toml b/contracts/sprungsui/Move.toml new file mode 100644 index 0000000..dbcc71b --- /dev/null +++ b/contracts/sprungsui/Move.toml @@ -0,0 +1,14 @@ +[package] +name = "sprungsui" +edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move + +[dependencies] +Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" } + +[addresses] +sprungsui = "0x0" + +[dev-dependencies] + +[dev-addresses] + diff --git a/contracts/sprungsui/sources/sprungsui.move b/contracts/sprungsui/sources/sprungsui.move new file mode 100644 index 0000000..4eb30db --- /dev/null +++ b/contracts/sprungsui/sources/sprungsui.move @@ -0,0 +1,20 @@ +module sprungsui::sprungsui { + use sui::coin::{Self}; + + public struct SPRUNGSUI has drop {} + + fun init(witness: SPRUNGSUI, ctx: &mut TxContext) { + let (treasury, metadata) = coin::create_currency( + witness, + 9, + b"", + b"Staked SUI", + b"", + option::none(), + ctx + ); + + transfer::public_share_object(metadata); + transfer::public_transfer(treasury, ctx.sender()) + } +} diff --git a/contracts/suilend/Move.lock b/contracts/suilend/Move.lock index d397ba8..e4085b3 100644 --- a/contracts/suilend/Move.lock +++ b/contracts/suilend/Move.lock @@ -2,11 +2,12 @@ [move] version = 3 -manifest_digest = "97E92C3AE2671D15B98EDF2F75D00F01F060C660AA87BCA5FB95A6792D62C242" -deps_digest = "3C4103934B1E040BB6B23F1D610B4EF9F2F1166A50A104EADCF77467C004C600" +manifest_digest = "35F92FCDECB247F47BD4304AA29525DA91F7CF4EBCB8777527B825341BB52706" +deps_digest = "060AD7E57DFB13104F21BE5F5C3759D03F0553FC3229247D9A7A6B45F50D03A3" dependencies = [ { id = "Pyth", name = "Pyth" }, { id = "Sui", name = "Sui" }, + { id = "liquid_staking", name = "liquid_staking" }, ] [[move.package]] @@ -30,6 +31,15 @@ dependencies = [ { id = "MoveStdlib", name = "MoveStdlib" }, ] +[[move.package]] +id = "SuiSystem" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/mainnet", subdir = "crates/sui-framework/packages/sui-system" } + +dependencies = [ + { id = "MoveStdlib", name = "MoveStdlib" }, + { id = "Sui", name = "Sui" }, +] + [[move.package]] id = "Wormhole" source = { git = "https://github.com/solendprotocol/wormhole.git", rev = "e1698d3c72b15cdddd7da98ad43e151f83b72a0a", subdir = "sui/wormhole" } @@ -38,6 +48,15 @@ dependencies = [ { id = "Sui", name = "Sui" }, ] +[[move.package]] +id = "liquid_staking" +source = { git = "https://github.com/solendprotocol/liquid-staking.git", rev = "main", subdir = "contracts" } + +dependencies = [ + { id = "Sui", name = "Sui" }, + { id = "SuiSystem", name = "SuiSystem" }, +] + [move.toolchain-version] compiler-version = "1.35.3" edition = "2024.beta" diff --git a/contracts/suilend/Move.toml b/contracts/suilend/Move.toml index c083578..7557900 100644 --- a/contracts/suilend/Move.toml +++ b/contracts/suilend/Move.toml @@ -15,6 +15,14 @@ git = "https://github.com/solendprotocol/pyth-crosschain.git" subdir = "target_chains/sui/contracts" rev = "98e218c64bb75cf1350eb7b021e1ffcc3aedfd62" +[dependencies.liquid_staking] +git = "https://github.com/solendprotocol/liquid-staking.git" +subdir = "contracts" +rev = "main" + +[dependencies.sprungsui] +local = "../sprungsui" + [addresses] sui = "0x2" # suilend = "0x0" diff --git a/contracts/suilend/sources/lending_market.move b/contracts/suilend/sources/lending_market.move index 875ec74..18a3462 100644 --- a/contracts/suilend/sources/lending_market.move +++ b/contracts/suilend/sources/lending_market.move @@ -1,6 +1,7 @@ module suilend::lending_market { // === Imports === use sui::object::{Self, ID, UID}; + use sui_system::sui_system::{SuiSystemState}; use suilend::rate_limiter::{Self, RateLimiter, RateLimiterConfig}; use std::ascii::{Self}; use sui::event::{Self}; @@ -10,10 +11,10 @@ module suilend::lending_market { use sui::clock::{Self, Clock}; use sui::tx_context::{Self, TxContext}; use sui::transfer; - use suilend::reserve::{Self, Reserve, CToken}; + use suilend::reserve::{Self, Reserve, CToken, LiquidityRequest}; use suilend::reserve_config::{ReserveConfig, borrow_fee}; use suilend::obligation::{Self, Obligation}; - use sui::coin::{Self, Coin, CoinMetadata}; + use sui::coin::{Self, Coin, CoinMetadata, TreasuryCap}; use sui::balance::{Self}; use pyth::price_info::{PriceInfoObject}; use std::type_name::{Self, TypeName}; @@ -21,6 +22,7 @@ module suilend::lending_market { use std::option::{Self, Option}; use suilend::liquidity_mining::{Self}; use sui::package; + use sui::sui::SUI; // === Errors === const EIncorrectVersion: u64 = 1; @@ -258,6 +260,26 @@ module suilend::lending_market { mut rate_limiter_exemption: Option>, ctx: &mut TxContext ): Coin { + let liquidity_request = redeem_ctokens_and_withdraw_liquidity_request( + lending_market, + reserve_array_index, + clock, + ctokens, + rate_limiter_exemption, + ctx + ); + + fulfill_liquidity_request(lending_market, reserve_array_index, liquidity_request, ctx) + } + + public fun redeem_ctokens_and_withdraw_liquidity_request( + lending_market: &mut LendingMarket

, + reserve_array_index: u64, + clock: &Clock, + ctokens: Coin>, + mut rate_limiter_exemption: Option>, + ctx: &mut TxContext + ): LiquidityRequest { let lending_market_id = object::id_address(lending_market); assert!(lending_market.version == CURRENT_VERSION, EIncorrectVersion); assert!(coin::value(&ctokens) > 0, ETooSmall); @@ -285,25 +307,24 @@ module suilend::lending_market { ); }; - let liquidity = reserve::redeem_ctokens( + let liquidity_request = reserve::redeem_ctokens( reserve, coin::into_balance(ctokens) ); - assert!(balance::value(&liquidity) > 0, ETooSmall); + assert!(reserve::liquidity_request_amount(&liquidity_request) > 0, ETooSmall); event::emit(RedeemEvent { lending_market_id, coin_type: type_name::get(), reserve_id: object::id_address(reserve), ctoken_amount, - liquidity_amount: balance::value(&liquidity), + liquidity_amount: reserve::liquidity_request_amount(&liquidity_request), }); - coin::from_balance(liquidity, ctx) + liquidity_request } - public fun deposit_ctokens_into_obligation( lending_market: &mut LendingMarket

, reserve_array_index: u64, @@ -323,7 +344,6 @@ module suilend::lending_market { ) } - /// Borrow tokens of type T. A fee is charged. public fun borrow( lending_market: &mut LendingMarket

, @@ -333,6 +353,25 @@ module suilend::lending_market { mut amount: u64, ctx: &mut TxContext ): Coin { + let liquidity_request = borrow_request( + lending_market, + reserve_array_index, + obligation_owner_cap, + clock, + amount + ); + + fulfill_liquidity_request(lending_market, reserve_array_index, liquidity_request, ctx) + } + + /// Borrow tokens of type T. A fee is charged. + public fun borrow_request( + lending_market: &mut LendingMarket

, + reserve_array_index: u64, + obligation_owner_cap: &ObligationOwnerCap

, + clock: &Clock, + mut amount: u64, + ): LiquidityRequest { let lending_market_id = object::id_address(lending_market); assert!(lending_market.version == CURRENT_VERSION, EIncorrectVersion); assert!(amount > 0, ETooSmall); @@ -354,11 +393,15 @@ module suilend::lending_market { assert!(amount > 0, ETooSmall); }; - let (receive_balance, borrow_amount_with_fees) = reserve::borrow_liquidity(reserve, amount); - let origination_fee_amount = borrow_amount_with_fees - balance::value(&receive_balance); - obligation::borrow

(obligation, reserve, clock, borrow_amount_with_fees); + let liquidity_request = reserve::borrow_liquidity(reserve, amount); + obligation::borrow

( + obligation, + reserve, + clock, + reserve::liquidity_request_amount(&liquidity_request) + ); - let borrow_value = reserve::market_value_upper_bound(reserve, decimal::from(borrow_amount_with_fees)); + let borrow_value = reserve::market_value_upper_bound(reserve, decimal::from(reserve::liquidity_request_amount(&liquidity_request))); rate_limiter::process_qty( &mut lending_market.rate_limiter, clock::timestamp_ms(clock) / 1000, @@ -370,12 +413,29 @@ module suilend::lending_market { coin_type: type_name::get(), reserve_id: object::id_address(reserve), obligation_id: object::id_address(obligation), - liquidity_amount: borrow_amount_with_fees, - origination_fee_amount, + liquidity_amount: reserve::liquidity_request_amount(&liquidity_request), + origination_fee_amount: reserve::liquidity_request_fee(&liquidity_request), }); obligation::zero_out_rewards_if_looped(obligation, &mut lending_market.reserves, clock); - coin::from_balance(receive_balance, ctx) + liquidity_request + } + + public fun fulfill_liquidity_request( + lending_market: &mut LendingMarket

, + reserve_array_index: u64, + liquidity_request: LiquidityRequest, + ctx: &mut TxContext + ): Coin { + 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); + + coin::from_balance( + reserve::fulfill_liquidity_request(reserve, liquidity_request), + ctx + ) } public fun withdraw_ctokens( @@ -664,6 +724,52 @@ module suilend::lending_market { } } + /* Staker operations */ + public fun init_staker( + lending_market: &mut LendingMarket

, + _: &LendingMarketOwnerCap

, + sui_reserve_array_index: u64, + treasury_cap: TreasuryCap, + ctx: &mut TxContext + ) { + assert!(lending_market.version == CURRENT_VERSION, EIncorrectVersion); + + let reserve = vector::borrow_mut(&mut lending_market.reserves, sui_reserve_array_index); + assert!(reserve::coin_type(reserve) == type_name::get(), EWrongType); + + reserve::init_staker(reserve, treasury_cap, ctx); + } + + public fun rebalance_staker

( + lending_market: &mut LendingMarket

, + sui_reserve_array_index: u64, + system_state: &mut SuiSystemState, + ctx: &mut TxContext + ) { + assert!(lending_market.version == CURRENT_VERSION, EIncorrectVersion); + + let reserve = vector::borrow_mut(&mut lending_market.reserves, sui_reserve_array_index); + assert!(reserve::coin_type(reserve) == type_name::get(), EWrongType); + + reserve::rebalance_staker

(reserve, system_state, ctx); + } + + public fun unstake_sui_from_staker

( + lending_market: &mut LendingMarket

, + sui_reserve_array_index: u64, + liquidity_request: &LiquidityRequest, + system_state: &mut SuiSystemState, + ctx: &mut TxContext + ) { + assert!(lending_market.version == CURRENT_VERSION, EIncorrectVersion); + + let reserve = vector::borrow_mut(&mut lending_market.reserves, sui_reserve_array_index); + if (reserve::coin_type(reserve) != type_name::get()) { + return; + }; + + reserve::unstake_sui_from_staker

(reserve, liquidity_request, system_state, ctx); + } // === Public-View Functions === fun max_borrow_amount

( @@ -803,7 +909,7 @@ module suilend::lending_market { object::id(lending_market), config, vector::length(&lending_market.reserves), - coin_metadata, + coin::get_decimals(coin_metadata), price_info, clock, ctx @@ -1080,4 +1186,30 @@ module suilend::lending_market { let LendingMarketOwnerCap { id, lending_market_id: _ } = lending_market_owner_cap; object::delete(id); } + + #[test_only] + public fun add_reserve_for_testing( + _: &LendingMarketOwnerCap

, + lending_market: &mut LendingMarket

, + price_info: &PriceInfoObject, + config: ReserveConfig, + mint_decimals: u8, + clock: &Clock, + ctx: &mut TxContext + ) { + assert!(lending_market.version == CURRENT_VERSION, EIncorrectVersion); + assert!(reserve_array_index(lending_market) == vector::length(&lending_market.reserves), EDuplicateReserve); + + let reserve = reserve::create_reserve( + object::id(lending_market), + config, + vector::length(&lending_market.reserves), + mint_decimals, + price_info, + clock, + ctx + ); + + vector::push_back(&mut lending_market.reserves, reserve); + } } diff --git a/contracts/suilend/sources/reserve.move b/contracts/suilend/sources/reserve.move index 929e043..591c550 100644 --- a/contracts/suilend/sources/reserve.move +++ b/contracts/suilend/sources/reserve.move @@ -1,6 +1,7 @@ /// The reserve module holds the coins of a certain type for a given lending market. module suilend::reserve { // === Imports === + use sui::sui::SUI; use std::type_name::{Self, TypeName}; use sui::dynamic_field::{Self}; use sui::balance::{Self, Balance, Supply}; @@ -12,7 +13,7 @@ module suilend::reserve { use suilend::oracles::{Self}; use suilend::decimal::{Decimal, Self, add, sub, mul, div, eq, floor, pow, le, ceil, min, max, saturating_sub}; use sui::clock::{Self, Clock}; - use sui::coin::{Self, CoinMetadata}; + use sui::coin::{Self, CoinMetadata, TreasuryCap}; use sui::math::{Self}; use pyth::price_identifier::{PriceIdentifier}; use pyth::price_info::{PriceInfoObject}; @@ -31,6 +32,9 @@ module suilend::reserve { liquidation_bonus }; use suilend::liquidity_mining::{Self, PoolRewardManager}; + use suilend::staker::{Self, Staker}; + use sui_system::sui_system::{SuiSystemState}; + use sprungsui::sprungsui::SPRUNGSUI; // === Errors === const EPriceStale: u64 = 0; @@ -40,7 +44,9 @@ module suilend::reserve { const EInvalidPrice: u64 = 4; const EMinAvailableAmountViolated: u64 = 5; const EInvalidRepayBalance: u64 = 6; - + const EWrongType: u64 = 7; + const EStakerAlreadyInitialized: u64 = 8; + const EStakerNotInitialized: u64 = 9; // === Constants === const PRICE_STALENESS_THRESHOLD_S: u64 = 0; // to prevent certain rounding bug attacks, we make sure that X amount of the underlying token amount @@ -85,8 +91,15 @@ module suilend::reserve { /// the underlying token + any interest earned. public struct CToken has drop {} + /// A request to withdraw liquidity from the reserve. This is a hot potato object. + public struct LiquidityRequest { + amount: u64, // includes fee + fee: u64, + } + // === Dynamic Field Keys === public struct BalanceKey has copy, drop, store {} + public struct StakerKey has copy, drop, store {} /// Balances are stored in a dynamic field to avoid typing the Reserve with CoinType public struct Balances has store { @@ -137,13 +150,19 @@ module suilend::reserve { price_last_update_timestamp_s: u64, } + public struct ClaimStakingRewardsEvent has drop, copy { + lending_market_id: address, + coin_type: TypeName, + reserve_id: address, + amount: u64, + } // === Conpublic structor === public(package) fun create_reserve( lending_market_id: ID, config: ReserveConfig, array_index: u64, - coin_metadata: &CoinMetadata, + mint_decimals: u8, price_info_obj: &PriceInfoObject, clock: &Clock, ctx: &mut TxContext @@ -158,7 +177,7 @@ module suilend::reserve { array_index, coin_type: type_name::get(), config: cell::new(config), - mint_decimals: coin::get_decimals(coin_metadata), + mint_decimals, price_identifier, price: option::extract(&mut price_decimal), smoothed_price: smoothed_price_decimal, @@ -472,6 +491,17 @@ module suilend::reserve { &balances.ctoken_fees } + public(package) fun liquidity_request_amount(request: &LiquidityRequest): u64 { + request.amount + } + public(package) fun liquidity_request_fee(request: &LiquidityRequest): u64 { + request.fee + } + + public fun staker(reserve: &Reserve

): &Staker { + dynamic_field::borrow(&reserve.id, StakerKey {}) + } + // === Public-Mutative Functions public(package) fun deposits_pool_reward_manager_mut

(reserve: &mut Reserve

): &mut PoolRewardManager { &mut reserve.deposits_pool_reward_manager @@ -656,7 +686,7 @@ module suilend::reserve { public(package) fun redeem_ctokens( reserve: &mut Reserve

, ctokens: Balance> - ): Balance { + ): LiquidityRequest { let ctoken_ratio = ctoken_ratio(reserve); let liquidity_amount = floor(mul( decimal::from(balance::value(&ctokens)), @@ -678,14 +708,117 @@ module suilend::reserve { ); balance::decrease_supply(&mut balances.ctoken_supply, ctokens); - balance::split(&mut balances.available_amount, liquidity_amount) + + LiquidityRequest { + amount: liquidity_amount, + fee: 0 + } + } + + public(package) fun fulfill_liquidity_request( + reserve: &mut Reserve

, + request: LiquidityRequest, + ): Balance { + let LiquidityRequest { amount, fee } = request; + + let balances: &mut Balances = dynamic_field::borrow_mut( + &mut reserve.id, + BalanceKey {} + ); + + let mut liquidity = balance::split(&mut balances.available_amount, amount); + balance::join(&mut balances.fees, balance::split(&mut liquidity, fee)); + + liquidity + } + + public(package) fun init_staker( + reserve: &mut Reserve

, + treasury_cap: TreasuryCap, + ctx: &mut TxContext + ) { + assert!(!dynamic_field::exists_(&reserve.id, StakerKey {}), EStakerAlreadyInitialized); + assert!(type_name::get() == type_name::get(), EWrongType); + + let staker = staker::create_staker(treasury_cap, ctx); + dynamic_field::add(&mut reserve.id, StakerKey {}, staker); + } + + public(package) fun rebalance_staker

( + reserve: &mut Reserve

, + system_state: &mut SuiSystemState, + ctx: &mut TxContext + ) { + assert!(dynamic_field::exists_(&reserve.id, StakerKey {}), EStakerNotInitialized); + let balances: &mut Balances = dynamic_field::borrow_mut( + &mut reserve.id, + BalanceKey {} + ); + let sui = balance::withdraw_all(&mut balances.available_amount); + + let staker: &mut Staker = dynamic_field::borrow_mut(&mut reserve.id, StakerKey {}); + + staker::deposit(staker, sui); + staker::rebalance(staker, system_state, ctx); + + let fees = staker::claim_fees(staker, system_state, ctx); + if (balance::value(&fees) > 0) { + event::emit(ClaimStakingRewardsEvent { + lending_market_id: object::id_to_address(&reserve.lending_market_id), + coin_type: reserve.coin_type, + reserve_id: object::uid_to_address(&reserve.id), + amount: balance::value(&fees), + }); + + let balances: &mut Balances = dynamic_field::borrow_mut( + &mut reserve.id, + BalanceKey {} + ); + + balance::join(&mut balances.fees, fees); + } + else { + balance::destroy_zero(fees); + }; + } + + public(package) fun unstake_sui_from_staker

( + reserve: &mut Reserve

, + liquidity_request: &LiquidityRequest, + system_state: &mut SuiSystemState, + ctx: &mut TxContext + ) { + assert!(reserve.coin_type == type_name::get(), EWrongType); + if (!dynamic_field::exists_(&reserve.id, StakerKey {})) { + return + }; + + let balances: &Balances = dynamic_field::borrow(&reserve.id, BalanceKey {}); + if (liquidity_request.amount <= balance::value(&balances.available_amount)) { + return + }; + let withdraw_amount = liquidity_request.amount - balance::value(&balances.available_amount); + + let staker: &mut Staker = dynamic_field::borrow_mut(&mut reserve.id, StakerKey {}); + let sui = staker::withdraw( + staker, + withdraw_amount, + system_state, + ctx + ); + + let balances: &mut Balances = dynamic_field::borrow_mut( + &mut reserve.id, + BalanceKey {} + ); + balance::join(&mut balances.available_amount, sui); } /// Borrow tokens from the reserve. A fee is charged on the borrowed amount public(package) fun borrow_liquidity( reserve: &mut Reserve

, amount: u64 - ): (Balance, u64) { + ): LiquidityRequest { let borrow_fee = calculate_borrow_fee(reserve, amount); let borrow_amount_with_fees = amount + borrow_fee; @@ -712,16 +845,11 @@ module suilend::reserve { ); log_reserve_data(reserve); - let balances: &mut Balances = dynamic_field::borrow_mut( - &mut reserve.id, - BalanceKey {} - ); - let mut receive_balance = balance::split(&mut balances.available_amount, borrow_amount_with_fees); - let fee_balance = balance::split(&mut receive_balance, borrow_fee); - balance::join(&mut balances.fees, fee_balance); - - (receive_balance, borrow_amount_with_fees) + LiquidityRequest { + amount: borrow_amount_with_fees, + fee: borrow_fee + } } public(package) fun repay_liquidity( diff --git a/contracts/suilend/sources/staker.move b/contracts/suilend/sources/staker.move new file mode 100644 index 0000000..f13dc4a --- /dev/null +++ b/contracts/suilend/sources/staker.move @@ -0,0 +1,190 @@ +/// Stake unlent Sui. +module suilend::staker { + use liquid_staking::liquid_staking::{LiquidStakingInfo, AdminCap, Self}; + use liquid_staking::fees::{Self}; + use sui::balance::{Self, Balance}; + use sui::tx_context::{TxContext}; + use sui::coin::{Self, TreasuryCap}; + use sui_system::sui_system::{SuiSystemState}; + use sui::sui::SUI; + use std::option::{Self, Option}; + use sui::transfer::Self; + + // errors + const ETreasuryCapNonZeroSupply: u64 = 0; + const EInvariantViolation: u64 = 1; + + // constants + const U64_MAX: u64 = 18446744073709551615; + const SUILEND_VALIDATOR: address = @0xce8e537664ba5d1d5a6a857b17bd142097138706281882be6805e17065ecde89; + + // This is mostly so i don't hit the "zero lst coin mint" error. + const MIN_DEPLOY_AMOUNT: u64 = 1_000_000; // 1 SUI + const MIST_PER_SUI: u64 = 1_000_000_000; + + public struct Staker has store { + admin: AdminCap

, + liquid_staking_info: LiquidStakingInfo

, + lst_balance: Balance

, + sui_balance: Balance, + liabilities: u64, // how much sui is owed to the reserve + } + + /* Public-View Functions */ + public(package) fun liabilities

(staker: &Staker

): u64 { + staker.liabilities + } + + public(package) fun lst_balance

(staker: &Staker

): &Balance

{ + &staker.lst_balance + } + + public(package) fun sui_balance

(staker: &Staker

): &Balance { + &staker.sui_balance + } + + // this value can be stale if the staker hasn't refreshed the liquid_staking_info + public(package) fun total_sui_supply

(staker: &Staker

): u64 { + staker.liquid_staking_info.total_sui_supply() + staker.sui_balance.value() + } + + public(package) fun liquid_staking_info

(staker: &Staker

): &LiquidStakingInfo

{ + &staker.liquid_staking_info + } + + /* Public Mutative Functions */ + public(package) fun create_staker( + treasury_cap: TreasuryCap

, + ctx: &mut TxContext + ): Staker

{ + assert!(coin::total_supply(&treasury_cap) == 0, ETreasuryCapNonZeroSupply); + + let (admin_cap, liquid_staking_info) = liquid_staking::create_lst( + fees::new_builder(ctx).to_fee_config(), + treasury_cap, + ctx + ); + + Staker { + admin: admin_cap, + liquid_staking_info, + lst_balance: balance::zero(), + sui_balance: balance::zero(), + liabilities: 0, + } + } + + public(package) fun deposit

( + staker: &mut Staker

, + sui: Balance, + ) { + staker.liabilities = staker.liabilities + sui.value(); + staker.sui_balance.join(sui); + } + + public(package) fun withdraw( + staker: &mut Staker

, + withdraw_amount: u64, + system_state: &mut SuiSystemState, + ctx: &mut TxContext + ): Balance { + staker.liquid_staking_info.refresh(system_state, ctx); + + if (withdraw_amount > staker.sui_balance.value()) { + let unstake_amount = withdraw_amount - staker.sui_balance.value(); + staker.unstake_n_sui(system_state, unstake_amount, ctx); + }; + + let sui = staker.sui_balance.split(withdraw_amount); + staker.liabilities = staker.liabilities - sui.value(); + + sui + } + + public(package) fun rebalance( + staker: &mut Staker

, + system_state: &mut SuiSystemState, + ctx: &mut TxContext + ) { + staker.liquid_staking_info.refresh(system_state, ctx); + + if (staker.sui_balance.value() < MIN_DEPLOY_AMOUNT) { + return + }; + + let sui = staker.sui_balance.withdraw_all(); + let lst = staker.liquid_staking_info.mint( + system_state, + coin::from_balance(sui, ctx), + ctx + ); + staker.lst_balance.join(lst.into_balance()); + + staker.liquid_staking_info.increase_validator_stake( + &staker.admin, + system_state, + SUILEND_VALIDATOR, + U64_MAX, + ctx + ); + } + + public(package) fun claim_fees( + staker: &mut Staker

, + system_state: &mut SuiSystemState, + ctx: &mut TxContext + ): Balance { + staker.liquid_staking_info.refresh(system_state, ctx); + + let total_sui_supply = staker.total_sui_supply(); + + // leave 1 SUI extra, just in case + let excess_sui = if (total_sui_supply > staker.liabilities + MIST_PER_SUI) { + total_sui_supply - staker.liabilities - MIST_PER_SUI + } else { + 0 + }; + + if (excess_sui > staker.sui_balance.value()) { + let unstake_amount = excess_sui - staker.sui_balance.value(); + staker.unstake_n_sui(system_state, unstake_amount, ctx); + }; + + let sui = staker.sui_balance.split(excess_sui); + + assert!(staker.total_sui_supply() >= staker.liabilities, EInvariantViolation); + + sui + } + + /* Private Functions */ + + // liquid_staking_info must be refreshed before calling this + // this function can unstake slightly more sui than requested due to rounding. + fun unstake_n_sui( + staker: &mut Staker

, + system_state: &mut SuiSystemState, + sui_amount_out: u64, + ctx: &mut TxContext + ) { + if (sui_amount_out == 0) { + return + }; + + let total_sui_supply = (staker.liquid_staking_info.total_sui_supply() as u128); + let total_lst_supply = (staker.liquid_staking_info.total_lst_supply() as u128); + + // ceil lst redemption amount + let lst_to_redeem = ((sui_amount_out as u128) * total_lst_supply + total_sui_supply - 1) / total_sui_supply; + let lst = balance::split(&mut staker.lst_balance, (lst_to_redeem as u64)); + + let sui = liquid_staking::redeem( + &mut staker.liquid_staking_info, + coin::from_balance(lst, ctx), + system_state, + ctx + ); + + staker.sui_balance.join(sui.into_balance()); + } +} diff --git a/contracts/suilend/tests/lending_market_tests.move b/contracts/suilend/tests/lending_market_tests.move index 44a3587..872f19f 100644 --- a/contracts/suilend/tests/lending_market_tests.move +++ b/contracts/suilend/tests/lending_market_tests.move @@ -1,4 +1,5 @@ module suilend::lending_market_tests { + use sui_system::sui_system::{SuiSystemState}; use sui::test_scenario::{Self, Scenario}; use sui::object::{Self, ID, UID}; use suilend::rate_limiter::{Self, RateLimiter, RateLimiterConfig}; @@ -23,11 +24,13 @@ module suilend::lending_market_tests { use sui::package; use suilend::lending_market::{Self, create_lending_market, LendingMarketOwnerCap, LendingMarket}; use suilend::mock_pyth::{PriceState}; - + use sui::sui::SUI; + use sprungsui::sprungsui::SPRUNGSUI; public struct LENDING_MARKET has drop {} const U64_MAX: u64 = 18446744073709551615; + const MIST_PER_SUI: u64 = 1_000_000_000; #[test] fun test_create_lending_market() { @@ -132,10 +135,12 @@ module suilend::lending_market_tests { let mut prices = mock_pyth::init_state(test_scenario::ctx(scenario)); mock_pyth::register(&mut prices, test_scenario::ctx(scenario)); mock_pyth::register(&mut prices, test_scenario::ctx(scenario)); + mock_pyth::register(&mut prices, test_scenario::ctx(scenario)); let mut type_to_index = bag::new(test_scenario::ctx(scenario)); bag::add(&mut type_to_index, type_name::get(), 0); bag::add(&mut type_to_index, type_name::get(), 1); + bag::add(&mut type_to_index, type_name::get(), 2); lending_market::add_reserve( &owner_cap, @@ -157,6 +162,16 @@ module suilend::lending_market_tests { test_scenario::ctx(scenario) ); + lending_market::add_reserve_for_testing( + &owner_cap, + &mut lending_market, + mock_pyth::get_price_obj(&prices), + reserve_config::default_reserve_config(), + 9, + &clock, + test_scenario::ctx(scenario) + ); + if (bag::contains(&reserve_args, type_name::get())) { let ReserveArgs { config, initial_deposit } = bag::remove( &mut reserve_args, @@ -211,6 +226,33 @@ module suilend::lending_market_tests { test_utils::destroy(ctokens); }; + if (bag::contains(&reserve_args, type_name::get())) { + let ReserveArgs { config, initial_deposit } = bag::remove( + &mut reserve_args, + type_name::get() + ); + let coins = coin::mint_for_testing( + initial_deposit, + test_scenario::ctx(scenario) + ); + + let ctokens = lending_market::deposit_liquidity_and_mint_ctokens( + &mut lending_market, + 2, + &clock, + coins, + test_scenario::ctx(scenario) + ); + + lending_market::update_reserve_config( + &owner_cap, + &mut lending_market, + 2, + config + ); + + test_utils::destroy(ctokens); + }; test_utils::destroy(reserve_args); test_utils::destroy(metadata); @@ -1196,7 +1238,7 @@ module suilend::lending_market_tests { test_scenario::end(scenario); } - #[test] + #[test] public fun test_max_borrow() { use sui::test_utils::{Self}; use suilend::test_usdc::{TEST_USDC}; @@ -1621,4 +1663,380 @@ module suilend::lending_market_tests { test_utils::destroy(type_to_index); test_scenario::end(scenario); } -} \ No newline at end of file + + 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] + public fun test_staker_e2e_redeem() { + use sui::test_utils::{Self}; + use suilend::reserve_config::{Self, default_reserve_config}; + use suilend::test_usdc::{TEST_USDC}; + + 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( + &mut bag, + type_name::get(), + ReserveArgs { + config: default_reserve_config(), + initial_deposit: 100 * 1_000_000_000 + } + ); + + bag + }, &mut scenario); + + clock::set_for_testing(&mut clock, 1 * 1000); + let treasury_cap = coin::create_treasury_cap_for_testing(scenario.ctx()); + lending_market::init_staker( + &mut lending_market, + &owner_cap, + *bag::borrow(&type_to_index, type_name::get()), + treasury_cap, + test_scenario::ctx(&mut scenario) + ); + + let sui_reserve = lending_market::reserve(&lending_market); + let staker = reserve::staker(sui_reserve); + assert!(staker.total_sui_supply() == 0); + assert!(staker.liabilities() == 0); + + let mut system_state = test_scenario::take_shared(&scenario); + lending_market::rebalance_staker( + &mut lending_market, + *bag::borrow(&type_to_index, type_name::get()), + &mut system_state, + test_scenario::ctx(&mut scenario) + ); + + let sui_reserve = lending_market::reserve(&lending_market); + let staker = reserve::staker(sui_reserve); + assert!(staker.total_sui_supply() == 100 * MIST_PER_SUI); + assert!(staker.liabilities() == 100 * MIST_PER_SUI); + + let sui = coin::mint_for_testing(100 * 1_000_000_000, test_scenario::ctx(&mut scenario)); + let c_sui = lending_market::deposit_liquidity_and_mint_ctokens( + &mut lending_market, + *bag::borrow(&type_to_index, type_name::get()), + &clock, + sui, + test_scenario::ctx(&mut scenario) + ); + + lending_market::rebalance_staker( + &mut lending_market, + *bag::borrow(&type_to_index, type_name::get()), + &mut system_state, + test_scenario::ctx(&mut scenario) + ); + + let sui_reserve = lending_market::reserve(&lending_market); + let staker = reserve::staker(sui_reserve); + assert!(staker.total_sui_supply() == 200 * MIST_PER_SUI); + assert!(staker.liabilities() == 200 * MIST_PER_SUI); + + let sui_reserve = lending_market::reserve(&lending_market); + let staker = reserve::staker(sui_reserve); + std::debug::print(staker); + + let liquidity_request = lending_market::redeem_ctokens_and_withdraw_liquidity_request( + &mut lending_market, + *bag::borrow(&type_to_index, type_name::get()), + &clock, + c_sui, + option::none(), + test_scenario::ctx(&mut scenario) + ); + + lending_market::unstake_sui_from_staker( + &mut lending_market, + *bag::borrow(&type_to_index, type_name::get()), + &liquidity_request, + &mut system_state, + test_scenario::ctx(&mut scenario) + ); + + let sui = lending_market::fulfill_liquidity_request( + &mut lending_market, + *bag::borrow(&type_to_index, type_name::get()), + liquidity_request, + test_scenario::ctx(&mut scenario) + ); + assert!(coin::value(&sui) == 100 * MIST_PER_SUI, 0); + + let sui_reserve = lending_market::reserve(&lending_market); + let staker = reserve::staker(sui_reserve); + assert!(staker.total_sui_supply() == 100 * MIST_PER_SUI); + assert!(staker.liabilities() == 100 * MIST_PER_SUI); + + test_scenario::return_shared(system_state); + + test_utils::destroy(sui); + 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] + public fun test_staker_e2e_borrow() { + use sui::test_utils::{Self}; + use suilend::reserve_config::{Self, default_reserve_config}; + use suilend::test_usdc::{TEST_USDC}; + use suilend::mock_pyth::{Self}; + + 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( + &mut bag, + type_name::get(), + ReserveArgs { + config: { + let reserve_config = default_reserve_config(); + let mut builder = reserve_config::from( + &reserve_config, + test_scenario::ctx(&mut scenario) + ); + builder.set_borrow_fee_bps(100); + + sui::test_utils::destroy(reserve_config); + + builder.build(scenario.ctx()) + }, + initial_deposit: 100 * 1_000_000_000 + } + ); + bag::add( + &mut bag, + type_name::get(), + ReserveArgs { + config: { + let reserve_config = default_reserve_config(); + let mut builder = reserve_config::from( + &reserve_config, + test_scenario::ctx(&mut scenario) + ); + builder.set_open_ltv_pct(50); + builder.set_close_ltv_pct(50); + builder.set_max_close_ltv_pct(50); + + sui::test_utils::destroy(reserve_config); + + builder.build(scenario.ctx()) + }, + initial_deposit: 100 * 1_000_000 + } + ); + + bag + }, &mut scenario); + + clock::set_for_testing(&mut clock, 1 * 1000); + let treasury_cap = coin::create_treasury_cap_for_testing(scenario.ctx()); + lending_market::init_staker( + &mut lending_market, + &owner_cap, + *bag::borrow(&type_to_index, type_name::get()), + treasury_cap, + test_scenario::ctx(&mut scenario) + ); + + let sui_reserve = lending_market::reserve(&lending_market); + let staker = reserve::staker(sui_reserve); + assert!(staker.total_sui_supply() == 0); + assert!(staker.liabilities() == 0); + + + let obligation_owner_cap = lending_market::create_obligation( + &mut lending_market, + test_scenario::ctx(&mut scenario) + ); + + let coins = coin::mint_for_testing(100 * 1_000_000, test_scenario::ctx(&mut scenario)); + let ctokens = lending_market::deposit_liquidity_and_mint_ctokens( + &mut lending_market, + *bag::borrow(&type_to_index, type_name::get()), + &clock, + coins, + test_scenario::ctx(&mut scenario) + ); + lending_market::deposit_ctokens_into_obligation( + &mut lending_market, + *bag::borrow(&type_to_index, type_name::get()), + &obligation_owner_cap, + &clock, + ctokens, + test_scenario::ctx(&mut scenario) + ); + + lending_market::refresh_reserve_price( + &mut lending_market, + *bag::borrow(&type_to_index, type_name::get()), + &clock, + mock_pyth::get_price_obj(&prices) + ); + lending_market::refresh_reserve_price( + &mut lending_market, + *bag::borrow(&type_to_index, type_name::get()), + &clock, + mock_pyth::get_price_obj(&prices) + ); + + let liquidity_request = lending_market::borrow_request( + &mut lending_market, + *bag::borrow(&type_to_index, type_name::get()), + &obligation_owner_cap, + &clock, + 1 * 1_000_000_000, + ); + assert!(reserve::liquidity_request_amount(&liquidity_request) == 1 * 1_000_000_000 + 10_000_000); + assert!(reserve::liquidity_request_fee(&liquidity_request) == 10_000_000); + + let mut system_state = test_scenario::take_shared(&scenario); + + lending_market::unstake_sui_from_staker( + &mut lending_market, + *bag::borrow(&type_to_index, type_name::get()), + &liquidity_request, + &mut system_state, + test_scenario::ctx(&mut scenario) + ); + + let sui = lending_market::fulfill_liquidity_request( + &mut lending_market, + *bag::borrow(&type_to_index, type_name::get()), + liquidity_request, + test_scenario::ctx(&mut scenario) + ); + assert!(coin::value(&sui) == 1 * MIST_PER_SUI, 0); + + test_scenario::return_shared(system_state); + + test_utils::destroy(sui); + 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); + } + + #[test] + public fun test_staker_e2e_claim_fees() { + use sui::test_utils::{Self}; + use suilend::reserve_config::{Self, default_reserve_config}; + use suilend::test_usdc::{TEST_USDC}; + + 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( + &mut bag, + type_name::get(), + ReserveArgs { + config: default_reserve_config(), + initial_deposit: 100 * 1_000_000_000 + } + ); + + bag + }, &mut scenario); + + clock::set_for_testing(&mut clock, 1 * 1000); + let treasury_cap = coin::create_treasury_cap_for_testing(scenario.ctx()); + lending_market::init_staker( + &mut lending_market, + &owner_cap, + *bag::borrow(&type_to_index, type_name::get()), + treasury_cap, + test_scenario::ctx(&mut scenario) + ); + + let sui_reserve = lending_market::reserve(&lending_market); + let staker = reserve::staker(sui_reserve); + assert!(staker.total_sui_supply() == 0); + assert!(staker.liabilities() == 0); + + let mut system_state = test_scenario::take_shared(&scenario); + lending_market::rebalance_staker( + &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); + + let sui_reserve = lending_market::reserve(&lending_market); + let staker = reserve::staker(sui_reserve); + assert!(staker.total_sui_supply() == 100 * MIST_PER_SUI); + assert!(staker.sui_balance().value() == 0); + assert!(staker.liabilities() == 100 * MIST_PER_SUI); + + advance_epoch_with_reward_amounts(0, 0, &mut scenario); + advance_epoch_with_reward_amounts(0, 100 , &mut scenario); + + let mut system_state = test_scenario::take_shared(&scenario); + lending_market::rebalance_staker( + &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); + + let sui_reserve = lending_market::reserve(&lending_market); + let staker = reserve::staker(sui_reserve); + std::debug::print(&staker.total_sui_supply()); + // the extra 50 sui gained has been transferred to the fees balance already + assert!(staker.total_sui_supply() == 101 * MIST_PER_SUI); + assert!(staker.liabilities() == 100 * MIST_PER_SUI); + + lending_market::claim_fees( + &mut lending_market, + *bag::borrow(&type_to_index, type_name::get()), + test_scenario::ctx(&mut scenario) + ); + + test_scenario::next_tx(&mut scenario, owner); + + let fees: Coin = test_scenario::take_from_address(&scenario, lending_market::fee_receiver(&lending_market)); + assert!(coin::value(&fees) == 49 * MIST_PER_SUI, 0); + + test_utils::destroy(fees); + + 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); + } +} diff --git a/contracts/suilend/tests/reserve_tests.move b/contracts/suilend/tests/reserve_tests.move index 4dd1225..af39bfd 100644 --- a/contracts/suilend/tests/reserve_tests.move +++ b/contracts/suilend/tests/reserve_tests.move @@ -186,7 +186,11 @@ module suilend::reserve_tests { let ctoken_supply_old = reserve.ctoken_supply(); let ctokens = balance::create_for_testing(10); - let tokens = redeem_ctokens(&mut reserve, ctokens); + let liquidity_request = redeem_ctokens(&mut reserve, ctokens); + assert!(reserve::liquidity_request_amount(&liquidity_request) == 50, 0); + assert!(reserve::liquidity_request_fee(&liquidity_request) == 0, 0); + + let tokens = reserve::fulfill_liquidity_request(&mut reserve, liquidity_request); assert!(balance::value(&tokens) == 50, 0); assert!(reserve.available_amount() == available_amount_old - 50, 0); @@ -241,9 +245,12 @@ module suilend::reserve_tests { let available_amount_old = reserve.available_amount(); let borrowed_amount_old = reserve.borrowed_amount(); - let (tokens, borrowed_amount_with_fee) = borrow_liquidity(&mut reserve, 400); + let liquidity_request = borrow_liquidity(&mut reserve, 400); + assert!(reserve::liquidity_request_amount(&liquidity_request) == 404, 0); + assert!(reserve::liquidity_request_fee(&liquidity_request) == 4, 0); + + let tokens = reserve::fulfill_liquidity_request(&mut reserve, liquidity_request); assert!(balance::value(&tokens) == 400, 0); - assert!(borrowed_amount_with_fee == 404, 0); assert!(reserve.available_amount() == available_amount_old - 404, 0); assert!(reserve.borrowed_amount() == add(borrowed_amount_old, decimal::from(404)), 0); @@ -302,10 +309,10 @@ module suilend::reserve_tests { balance::create_for_testing(1000) ); - let (tokens, _) = borrow_liquidity(&mut reserve, 1); + let liquidity_request = borrow_liquidity(&mut reserve, 1); + sui::test_utils::destroy(liquidity_request); sui::test_utils::destroy(reserve); - sui::test_utils::destroy(tokens); sui::test_utils::destroy(ctokens); test_scenario::end(scenario); @@ -347,10 +354,10 @@ module suilend::reserve_tests { balance::create_for_testing(10_000_000) ); - let (tokens, _) = borrow_liquidity(&mut reserve, 1_000_000 + 1); + let liquidity_request = borrow_liquidity(&mut reserve, 1_000_000 + 1); + sui::test_utils::destroy(liquidity_request); sui::test_utils::destroy(reserve); - sui::test_utils::destroy(tokens); sui::test_utils::destroy(ctokens); test_scenario::end(scenario); @@ -409,7 +416,8 @@ module suilend::reserve_tests { balance::create_for_testing(100 * 1_000_000) ); - let (tokens, _) = borrow_liquidity(&mut reserve, 50 * 1_000_000); + let liquidity_request = borrow_liquidity(&mut reserve, 50 * 1_000_000); + let tokens = reserve::fulfill_liquidity_request(&mut reserve, liquidity_request); clock::set_for_testing(&mut clock, 1000); compound_interest(&mut reserve, &clock); @@ -474,7 +482,8 @@ module suilend::reserve_tests { balance::create_for_testing(1000) ); - let (tokens, _) = borrow_liquidity(&mut reserve, 400); + let liquidity_request = borrow_liquidity(&mut reserve, 400); + let tokens = reserve::fulfill_liquidity_request(&mut reserve, liquidity_request); let available_amount_old = reserve.available_amount(); let borrowed_amount_old = reserve.borrowed_amount(); diff --git a/contracts/suilend/tests/staker_tests.move b/contracts/suilend/tests/staker_tests.move new file mode 100644 index 0000000..c6a3c23 --- /dev/null +++ b/contracts/suilend/tests/staker_tests.move @@ -0,0 +1,125 @@ +module suilend::staker_tests { + + public struct STAKER_TESTS has drop {} + + use sui::test_scenario::{Self, Scenario}; + use sui_system::governance_test_utils::{ + advance_epoch_with_reward_amounts, + create_validator_for_testing, + create_sui_system_state_for_testing, + }; + use sui::balance::{Self}; + use sui::coin::{Self}; + use suilend::staker::{create_staker}; + use sui_system::sui_system::{SuiSystemState}; + use sui::sui::{SUI}; + + /* Constants */ + const MIST_PER_SUI: u64 = 1_000_000_000; + const SUILEND_VALIDATOR: address = @0xce8e537664ba5d1d5a6a857b17bd142097138706281882be6805e17065ecde89; + + public struct STAKER has drop {} + + 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] + public fun test_end_to_end_happy() { + let owner = @0x26; + let mut scenario = test_scenario::begin(owner); + setup_sui_system(&mut scenario); + + let treasury_cap = coin::create_treasury_cap_for_testing(test_scenario::ctx(&mut scenario)); + + let mut staker = create_staker(treasury_cap, test_scenario::ctx(&mut scenario)); + assert!(staker.sui_balance().value() == 0, 0); + assert!(staker.lst_balance().value() == 0, 0); + assert!(staker.liabilities() == 0, 0); + + let mut system_state = test_scenario::take_shared(&scenario); + staker.rebalance(&mut system_state, scenario.ctx()); + + let sui = balance::create_for_testing(100 * MIST_PER_SUI); + staker.deposit(sui); + + assert!(staker.liabilities() == 100 * MIST_PER_SUI, 0); + assert!(staker.sui_balance().value() == 100 * MIST_PER_SUI, 0); + assert!(staker.lst_balance().value() == 0, 0); + + let sui = staker.withdraw(100 * MIST_PER_SUI, &mut system_state, scenario.ctx()); + assert!(sui.value() == 100 * MIST_PER_SUI, 0); + assert!(staker.liabilities() == 0, 0); + assert!(staker.sui_balance().value() == 0, 0); + assert!(staker.lst_balance().value() == 0, 0); + + staker.deposit(sui); + staker.rebalance(&mut system_state, scenario.ctx()); + + assert!(staker.liabilities() == 100 * MIST_PER_SUI, 0); + assert!(staker.sui_balance().value() == 0, 0); + assert!(staker.lst_balance().value() == 100 * MIST_PER_SUI, 0); + assert!(staker.total_sui_supply() == 100 * MIST_PER_SUI, 0); + assert!(staker.liquid_staking_info().total_sui_supply() == 100 * MIST_PER_SUI, 0); + + test_scenario::return_shared(system_state); + + advance_epoch_with_reward_amounts(0, 0, &mut scenario); + // 1 lst is worth 2 sui now + advance_epoch_with_reward_amounts(0, 200, &mut scenario); // 100 SUI + + let mut system_state = test_scenario::take_shared(&scenario); + + staker.rebalance(&mut system_state, scenario.ctx()); + assert!(staker.liabilities() == 100 * MIST_PER_SUI, 0); + assert!(staker.sui_balance().value() == 0, 0); + assert!(staker.liquid_staking_info().total_sui_supply() == 200 * MIST_PER_SUI, 0); + assert!(staker.lst_balance().value() == 100 * MIST_PER_SUI, 0); + assert!(staker.total_sui_supply() == 200 * MIST_PER_SUI, 0); + + let sui = staker.claim_fees(&mut system_state, scenario.ctx()); + assert!(sui.value() == 99 * MIST_PER_SUI, 0); + assert!(staker.liabilities() == 100 * MIST_PER_SUI, 0); + assert!(staker.sui_balance().value() == 0, 0); + assert!(staker.liquid_staking_info().total_sui_supply() == 101 * MIST_PER_SUI, 0); + assert!(staker.lst_balance().value() == 50 * MIST_PER_SUI + 500_000_000, 0); + assert!(staker.total_sui_supply() == 101 * MIST_PER_SUI, 0); + sui::test_utils::destroy(sui); + + // should be no fees to claim + let sui = staker.claim_fees(&mut system_state, scenario.ctx()); + assert!(sui.value() == 0, 0); + assert!(staker.liabilities() == 100 * MIST_PER_SUI, 0); + assert!(staker.sui_balance().value() == 0, 0); + assert!(staker.liquid_staking_info().total_sui_supply() == 101 * MIST_PER_SUI, 0); + assert!(staker.lst_balance().value() == 50 * MIST_PER_SUI + 500_000_000, 0); + assert!(staker.total_sui_supply() == 101 * MIST_PER_SUI, 0); + sui::test_utils::destroy(sui); + + let sui = staker.withdraw(MIST_PER_SUI + 1, &mut system_state, scenario.ctx()); + assert!(sui.value() == MIST_PER_SUI + 1, 0); + assert!(staker.liabilities() == 99 * MIST_PER_SUI - 1, 0); + assert!(staker.sui_balance().value() == 1, 0); + assert!(staker.liquid_staking_info().total_sui_supply() == 100 * MIST_PER_SUI - 2, 0); + assert!(staker.lst_balance().value() == 50 * MIST_PER_SUI - 1, 0); + assert!(staker.total_sui_supply() == 100 * MIST_PER_SUI - 1, 0); + sui::test_utils::destroy(sui); + + let sui = staker.claim_fees(&mut system_state, scenario.ctx()); + assert!(sui.value() == 0); + sui::test_utils::destroy(sui); + + let sui = staker.withdraw(0, &mut system_state, scenario.ctx()); + assert!(sui.value() == 0); + sui::test_utils::destroy(sui); + + test_scenario::return_shared(system_state); + sui::test_utils::destroy(staker); + test_scenario::end(scenario); + } + +}