Skip to content

Commit

Permalink
refactor staker module a little bit
Browse files Browse the repository at this point in the history
  • Loading branch information
0xripleys committed Nov 7, 2024
1 parent 37194fd commit 2b7c6ef
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 59 deletions.
128 changes: 85 additions & 43 deletions contracts/suilend/sources/staker.move
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// Stake unlent Sui.
module suilend::staker {
use liquid_staking::liquid_staking::{LiquidStakingInfo, AdminCap, Self, total_sui_supply, total_lst_supply};
use liquid_staking::liquid_staking::{LiquidStakingInfo, AdminCap, Self};
use liquid_staking::fees::{Self};
use sui::balance::{Self, Balance};
use sui::tx_context::{TxContext};
Expand All @@ -20,6 +20,9 @@ module suilend::staker {
// but we start at 50% for safety reasons.
const TARGET_UTIL_BPS: u64 = 5000; // 50%

// This is mostly so i don't hit the "zero lst coin mint" error.
const MIN_DEPLOY_AMOUNT: u64 = 1_000_000; // 1 SUI

public struct Staker<phantom P> has store {
admin: AdminCap<P>,
liquid_staking_info: LiquidStakingInfo<P>,
Expand All @@ -37,6 +40,19 @@ module suilend::staker {
&staker.lst_balance
}

public(package) fun sui_balance<P>(staker: &Staker<P>): &Balance<SUI> {
&staker.sui_balance
}

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

public(package) fun liquid_staking_info<P>(staker: &Staker<P>): &LiquidStakingInfo<P> {
&staker.liquid_staking_info
}

/* Public Mutative Functions */
public(package) fun create_staker<P: drop>(
treasury_cap: TreasuryCap<P>,
Expand Down Expand Up @@ -67,79 +83,107 @@ module suilend::staker {
staker.sui_balance.join(sui);
}

public(package) fun stake<P: drop>(
staker: &mut Staker<P>,
public(package) fun withdraw<P: drop>(
staker: &mut Staker<P>,
withdraw_amount: u64,
system_state: &mut SuiSystemState,
sui: Balance<SUI>,
ctx: &mut TxContext
) {
staker.liabilities = staker.liabilities + balance::value(&sui);
): Balance<SUI> {
staker.liquid_staking_info.refresh(system_state, ctx);

let lst = staker.liquid_staking_info.mint(
system_state,
coin::from_balance(sui, ctx),
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);
};

staker.liquid_staking_info.increase_validator_stake(
&staker.admin,
system_state,
SUILEND_VALIDATOR,
U64_MAX,
ctx
);
let sui = staker.sui_balance.split(withdraw_amount);
staker.liabilities = staker.liabilities - sui.value();

staker.lst_balance.join(lst.into_balance());
assert!(staker.liquid_staking_info.total_sui_supply() >= staker.liabilities, EInvariantViolation);
sui
}

// unstake sui. this function can return less than the requested amount due to rounding
public(package) fun unstake<P: drop>(
staker: &mut Staker<P>,
public(package) fun rebalance<P: drop>(
staker: &mut Staker<P>,
system_state: &mut SuiSystemState,
unstake_amount: u64,
ctx: &mut TxContext
): Balance<SUI> {
) {
staker.liquid_staking_info.refresh(system_state, ctx);

let sui = staker.withdraw_n_sui(system_state, unstake_amount, ctx);

staker.liabilities = staker.liabilities - unstake_amount;

assert!(staker.liquid_staking_info.total_sui_supply() >= staker.liabilities, EInvariantViolation);
let staked_sui = staker.liquid_staking_info.total_sui_supply();
let target_staked_sui = (staker.total_sui_supply() * TARGET_UTIL_BPS) / 10000;

if (target_staked_sui >= staked_sui + MIN_DEPLOY_AMOUNT) {
let sui = staker.sui_balance.split(target_staked_sui - staked_sui);
let lst = staker.liquid_staking_info.mint(
system_state,
coin::from_balance(sui, ctx),
ctx
);

staker.liquid_staking_info.increase_validator_stake(
&staker.admin,
system_state,
SUILEND_VALIDATOR,
U64_MAX,
ctx
);

staker.lst_balance.join(lst.into_balance());
}
else {
staker.unstake_n_sui(system_state, staked_sui - target_staked_sui, ctx);
};

sui
assert!(
staker.liquid_staking_info.total_sui_supply() + staker.sui_balance.value() >= staker.liabilities,
EInvariantViolation
);
}

public(package) fun claim_fees<P: drop>(
staker: &mut Staker<P>,
system_state: &mut SuiSystemState,
ctx: &mut TxContext
): Balance<SUI> {
liquid_staking::refresh(&mut staker.liquid_staking_info, system_state, ctx);
let total_sui_supply = total_sui_supply(&staker.liquid_staking_info);
staker.liquid_staking_info.refresh(system_state, ctx);

let total_sui_supply = staker.total_sui_supply();
let excess_sui = total_sui_supply - staker.liabilities;

let sui = withdraw_n_sui(staker, system_state, excess_sui, ctx);
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);
};

assert!(total_sui_supply(&staker.liquid_staking_info) >= staker.liabilities, EInvariantViolation);
let sui = staker.sui_balance.split(excess_sui);

assert!(
staker.liquid_staking_info.total_sui_supply() + staker.sui_balance.value() >= staker.liabilities,
EInvariantViolation
);

sui
}

/* Private Functions */

// liquid_staking_info must be refreshed before calling this
fun withdraw_n_sui<P: drop>(
// this function can unstake slightly more sui than requested due to rounding.
fun unstake_n_sui<P: drop>(
staker: &mut Staker<P>,
system_state: &mut SuiSystemState,
sui_amount_out: u64,
ctx: &mut TxContext
): Balance<SUI> {
let total_sui_supply = (total_sui_supply(&staker.liquid_staking_info) as u128);
let total_lst_supply = (total_lst_supply(&staker.liquid_staking_info) as u128);
) {
if (sui_amount_out == 0) {
return;
};

let lst_to_redeem = (sui_amount_out as u128) * total_lst_supply / total_sui_supply;
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(
Expand All @@ -149,8 +193,6 @@ module suilend::staker {
ctx
);

assert!(coin::value(&sui) <= sui_amount_out, EInvariantViolation);

coin::into_balance(sui)
staker.sui_balance.join(sui.into_balance());
}
}
56 changes: 40 additions & 16 deletions contracts/suilend/tests/staker_tests.move
Original file line number Diff line number Diff line change
Expand Up @@ -35,40 +35,64 @@ module suilend::staker_tests {
let treasury_cap = coin::create_treasury_cap_for_testing<STAKER_TESTS>(test_scenario::ctx(&mut scenario));

let mut staker = create_staker(treasury_cap, test_scenario::ctx(&mut scenario));
// TODO: check more stuff
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<SuiSystemState>(&scenario);
staker.rebalance(&mut system_state, scenario.ctx());

let sui = balance::create_for_testing<SUI>(100 * MIST_PER_SUI);
staker.stake(&mut system_state, sui, test_scenario::ctx(&mut scenario));
staker.deposit(sui);

assert!(staker.liabilities() == 100 * MIST_PER_SUI, 0);
assert!(staker.lst_balance().value() == 100 * MIST_PER_SUI, 0);
assert!(staker.sui_balance().value() == 100 * MIST_PER_SUI, 0);
assert!(staker.lst_balance().value() == 0, 0);

staker.rebalance(&mut system_state, scenario.ctx());

assert!(staker.liabilities() == 100 * MIST_PER_SUI, 0);
assert!(staker.sui_balance().value() == 50 * MIST_PER_SUI, 0);
assert!(staker.lst_balance().value() == 50 * 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
advance_epoch_with_reward_amounts(0, 150, &mut scenario); // 100 SUI

let mut system_state = test_scenario::take_shared<SuiSystemState>(&scenario);
let sui = staker.unstake(&mut system_state, 100 * MIST_PER_SUI, test_scenario::ctx(&mut scenario));

std::debug::print(&staker);

assert!(staker.liabilities() == 0, 0);
assert!(staker.lst_balance().value() == 50 * MIST_PER_SUI, 0);
assert!(sui.value() == 100 * MIST_PER_SUI, 0);

let fees = staker.claim_fees(&mut system_state, test_scenario::ctx(&mut scenario));
assert!(fees.value() == 100 * MIST_PER_SUI, 0);
// before rebalance:
// 100 sui staked, 50 sui in sui_balance
// after rebalance: 75, 75
staker.rebalance(&mut system_state, scenario.ctx());
assert!(staker.liabilities() == 100 * MIST_PER_SUI, 0);
assert!(staker.sui_balance().value() == 75 * MIST_PER_SUI, 0);
assert!(staker.liquid_staking_info().total_sui_supply() == 75 * MIST_PER_SUI, 0);
assert!(staker.lst_balance().value() == 75 * MIST_PER_SUI / 2, 0);
assert!(staker.total_sui_supply() == 150 * MIST_PER_SUI, 0);

std::debug::print(&staker);
let sui = staker.claim_fees(&mut system_state, scenario.ctx());
assert!(sui.value() == 50 * MIST_PER_SUI, 0);
assert!(staker.liabilities() == 100 * MIST_PER_SUI, 0);
assert!(staker.sui_balance().value() == 25 * MIST_PER_SUI, 0);
assert!(staker.liquid_staking_info().total_sui_supply() == 75 * MIST_PER_SUI, 0);
assert!(staker.lst_balance().value() == 75 * MIST_PER_SUI / 2, 0);
assert!(staker.total_sui_supply() == 100 * MIST_PER_SUI, 0);
sui::test_utils::destroy(sui);

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.liquid_staking_info().total_sui_supply() == 0, 0);
assert!(staker.lst_balance().value() == 0, 0);
assert!(staker.total_sui_supply() == 0, 0);
sui::test_utils::destroy(sui);
sui::test_utils::destroy(fees);
sui::test_utils::destroy(staker);

test_scenario::return_shared(system_state);
sui::test_utils::destroy(staker);
test_scenario::end(scenario);
}

Expand Down

0 comments on commit 2b7c6ef

Please sign in to comment.