From f88e8aea46ce5aa0a72837de0e8f11b9411d0997 Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Tue, 18 Jun 2024 16:26:23 -0400 Subject: [PATCH 01/10] fungible stake implementation --- .../packages/sui-system/Move.lock | 4 +- .../sui-system/sources/staking_pool.move | 281 ++++++++++++++++ .../sui-system/sources/sui_system.move | 22 +- .../sources/sui_system_state_inner.move | 18 +- .../sui-system/sources/validator.move | 18 +- .../sui-system/sources/validator_set.move | 41 ++- .../sui-system/tests/staking_pool.move | 302 ++++++++++++++++++ .../sui-system/tests/sui_system_tests.move | 55 ++++ 8 files changed, 735 insertions(+), 6 deletions(-) create mode 100644 crates/sui-framework/packages/sui-system/tests/staking_pool.move diff --git a/crates/sui-framework/packages/sui-system/Move.lock b/crates/sui-framework/packages/sui-system/Move.lock index af204d00c7990..a5b11a5ebb2b1 100644 --- a/crates/sui-framework/packages/sui-system/Move.lock +++ b/crates/sui-framework/packages/sui-system/Move.lock @@ -23,6 +23,6 @@ dependencies = [ ] [move.toolchain-version] -compiler-version = "1.22.0" -edition = "legacy" +compiler-version = "1.29.0" +edition = "2024.beta" flavor = "sui" diff --git a/crates/sui-framework/packages/sui-system/sources/staking_pool.move b/crates/sui-framework/packages/sui-system/sources/staking_pool.move index 0295dbfa703a1..774cdeaf86ff1 100644 --- a/crates/sui-framework/packages/sui-system/sources/staking_pool.move +++ b/crates/sui-framework/packages/sui-system/sources/staking_pool.move @@ -31,6 +31,8 @@ module sui_system::staking_pool { const EActivationOfInactivePool: u64 = 16; const EDelegationOfZeroSui: u64 = 17; const EStakedSuiBelowThreshold: u64 = 18; + const ECannotMintFungibleStakedSuiYet: u64 = 19; + const EInvariantFailure: u64 = 20; /// A staking pool embedded in each validator struct in the system state object. public struct StakingPool has key, store { @@ -80,6 +82,30 @@ module sui_system::staking_pool { principal: Balance, } + /// An alternative to `StakedSui` that holds the pool token amount instead of the SUI balance. + /// StakedSui objects can be converted to FungibleStakedSuis after the initial warmup period. + /// The advantage of this is that you can now merge multiple StakedSui objects from different + /// activation epochs into a single FungibleStakedSui object. + public struct FungibleStakedSui has key, store { + id: UID, + /// ID of the staking pool we are staking with. + pool_id: ID, + /// The pool token amount. + value: u64, + } + + /// Holds useful information + public struct FungibleStakedSuiData has key, store { + id: UID, + /// fungible_staked_sui supply + total_supply: u64, + /// principal balance. Rewards are withdrawn from the reward pool + principal: Balance, + } + + // === dynamic field keys === + public struct FungibleStakedSuiDataKey has copy, store, drop {} + // ==== initializer ==== /// Create a new, empty staking pool. @@ -158,6 +184,127 @@ module sui_system::staking_pool { principal_withdraw } + public(package) fun redeem_fungible_staked_sui( + pool: &mut StakingPool, + fungible_staked_sui: FungibleStakedSui, + ctx: &TxContext + ) : Balance { + let FungibleStakedSui { id, pool_id, value } = fungible_staked_sui; + assert!(pool_id == object::id(pool), EWrongPool); + + object::delete(id); + + let latest_exchange_rate = pool_token_exchange_rate_at_epoch(pool, tx_context::epoch(ctx)); + let fungible_staked_sui_data: &mut FungibleStakedSuiData = bag::borrow_mut( + &mut pool.extra_fields, + FungibleStakedSuiDataKey {} + ); + + let (principal_amount, rewards_amount) = calculate_fungible_staked_sui_withdraw_amount( + latest_exchange_rate, + value, + balance::value(&fungible_staked_sui_data.principal), + fungible_staked_sui_data.total_supply + ); + + fungible_staked_sui_data.total_supply = fungible_staked_sui_data.total_supply - value; + + let mut sui_out = balance::split(&mut fungible_staked_sui_data.principal, principal_amount); + balance::join( + &mut sui_out, + balance::split(&mut pool.rewards_pool, rewards_amount) + ); + + pool.pending_total_sui_withdraw = pool.pending_total_sui_withdraw + balance::value(&sui_out); + pool.pending_pool_token_withdraw = pool.pending_pool_token_withdraw + value; + + sui_out + } + + /// written in separate function so i can test with random values + /// returns (principal_withdraw_amount, rewards_withdraw_amount) + fun calculate_fungible_staked_sui_withdraw_amount( + latest_exchange_rate: PoolTokenExchangeRate, + fungible_staked_sui_value: u64, + fungible_staked_sui_data_principal_amount: u64, // fungible_staked_sui_data.principal.value() + fungible_staked_sui_data_total_supply: u64, // fungible_staked_sui_data.total_supply + ) : (u64, u64) { + // 1. if the entire FungibleStakedSuiData supply is redeemed, how much sui should we receive? + let total_sui_amount = get_sui_amount(&latest_exchange_rate, fungible_staked_sui_data_total_supply); + + // 2. how much do we need to withdraw from the rewards pool? + let total_rewards = total_sui_amount - fungible_staked_sui_data_principal_amount; + + // 3. proportionally withdraw from both wrt the fungible_staked_sui_value. + let principal_withdraw_amount = ((fungible_staked_sui_value as u128) + * (fungible_staked_sui_data_principal_amount as u128) + / (fungible_staked_sui_data_total_supply as u128)) as u64; + + let rewards_withdraw_amount = ((fungible_staked_sui_value as u128) + * (total_rewards as u128) + / (fungible_staked_sui_data_total_supply as u128)) as u64; + + // invariant check, just in case + let expected_sui_amount = get_sui_amount(&latest_exchange_rate, fungible_staked_sui_value); + assert!(principal_withdraw_amount + rewards_withdraw_amount <= expected_sui_amount, EInvariantFailure); + + (principal_withdraw_amount, rewards_withdraw_amount) + } + + /// Convert the given staked SUI to an FungibleStakedSui object + public(package) fun convert_to_fungible_staked_sui( + pool: &mut StakingPool, + staked_sui: StakedSui, + ctx: &mut TxContext + ) : FungibleStakedSui { + let StakedSui { id, pool_id, stake_activation_epoch, principal } = staked_sui; + + assert!(pool_id == object::id(pool), EWrongPool); + assert!( + tx_context::epoch(ctx) >= stake_activation_epoch, + ECannotMintFungibleStakedSuiYet + ); + + object::delete(id); + + + let exchange_rate_at_staking_epoch = pool_token_exchange_rate_at_epoch( + pool, + stake_activation_epoch + ); + + let pool_token_amount = get_token_amount( + &exchange_rate_at_staking_epoch, + balance::value(&principal) + ); + + if (!bag::contains(&pool.extra_fields, FungibleStakedSuiDataKey {})) { + bag::add( + &mut pool.extra_fields, + FungibleStakedSuiDataKey {}, + FungibleStakedSuiData { + id: object::new(ctx), + total_supply: pool_token_amount, + principal + } + ); + } + else { + let fungible_staked_sui_data: &mut FungibleStakedSuiData = bag::borrow_mut( + &mut pool.extra_fields, + FungibleStakedSuiDataKey {} + ); + fungible_staked_sui_data.total_supply = fungible_staked_sui_data.total_supply + pool_token_amount; + balance::join(&mut fungible_staked_sui_data.principal, principal); + }; + + FungibleStakedSui { + id: object::new(ctx), + pool_id, + value: pool_token_amount, + } + } + /// Withdraw the principal SUI stored in the StakedSui object, and calculate the corresponding amount of pool /// tokens using exchange rate at staking epoch. /// Returns values are amount of pool tokens withdrawn and withdrawn principal portion of SUI. @@ -293,6 +440,9 @@ module sui_system::staking_pool { public fun pool_id(staked_sui: &StakedSui): ID { staked_sui.pool_id } + public use fun fungible_staked_sui_pool_id as FungibleStakedSui.pool_id; + public fun fungible_staked_sui_pool_id(fungible_staked_sui: &FungibleStakedSui): ID { fungible_staked_sui.pool_id } + public fun staked_sui_amount(staked_sui: &StakedSui): u64 { staked_sui.principal.value() } /// Allows calling `.amount()` on `StakedSui` to invoke `staked_sui_amount` @@ -312,6 +462,39 @@ module sui_system::staking_pool { pool.deactivation_epoch.is_some() } + public use fun fungible_staked_sui_value as FungibleStakedSui.value; + public fun fungible_staked_sui_value(fungible_staked_sui: &FungibleStakedSui): u64 { fungible_staked_sui.value } + + public use fun split_fungible_staked_sui as FungibleStakedSui.split; + public fun split_fungible_staked_sui( + fungible_staked_sui: &mut FungibleStakedSui, + split_amount: u64, + ctx: &mut TxContext + ): FungibleStakedSui { + assert!(split_amount <= fungible_staked_sui.value, EInsufficientPoolTokenBalance); + + fungible_staked_sui.value = fungible_staked_sui.value - split_amount; + + assert!(fungible_staked_sui.value >= MIN_STAKING_THRESHOLD, EStakedSuiBelowThreshold); + assert!(split_amount >= MIN_STAKING_THRESHOLD, EStakedSuiBelowThreshold); + + FungibleStakedSui { + id: object::new(ctx), + pool_id: fungible_staked_sui.pool_id, + value: split_amount, + } + } + + public use fun join_fungible_staked_sui as FungibleStakedSui.join; + public fun join_fungible_staked_sui(self: &mut FungibleStakedSui, other: FungibleStakedSui) { + let FungibleStakedSui { id, pool_id, value } = other; + assert!(self.pool_id == pool_id, EWrongPool); + + object::delete(id); + + self.value = self.value + value; + } + /// Split StakedSui `self` to two parts, one with principal `split_amount`, /// and the remaining principal is left in `self`. /// All the other parameters of the StakedSui like `stake_activation_epoch` or `pool_id` remain the same. @@ -473,4 +656,102 @@ module sui_system::staking_pool { staked_amount + reward_withdraw_amount } + + #[test_only] + public(package) fun fungible_staked_sui_data(pool: &StakingPool): &FungibleStakedSuiData { + bag::borrow(&pool.extra_fields, FungibleStakedSuiDataKey {}) + } + + public use fun fungible_staked_sui_data_total_supply as FungibleStakedSuiData.total_supply; + + #[test_only] + public(package) fun fungible_staked_sui_data_total_supply(fungible_staked_sui_data: &FungibleStakedSuiData): u64 { + fungible_staked_sui_data.total_supply + } + + public use fun fungible_staked_sui_data_principal_value as FungibleStakedSuiData.principal_value; + + #[test_only] + public(package) fun fungible_staked_sui_data_principal_value(fungible_staked_sui_data: &FungibleStakedSuiData): u64 { + fungible_staked_sui_data.principal.value() + } + + #[test_only] + public(package) fun pending_pool_token_withdraw_amount(pool: &StakingPool): u64 { + pool.pending_pool_token_withdraw + } + + #[test_only] + public(package) fun create_fungible_staked_sui_for_testing( + self: &StakingPool, + value: u64, + ctx: &mut TxContext + ) : FungibleStakedSui { + FungibleStakedSui { + id: object::new(ctx), + pool_id: object::id(self), + value, + } + } + + // ==== tests ==== + + #[random_test] + fun test_calculate_fungible_staked_sui_withdraw_amount( + mut total_sui_amount: u64, + // these are all in basis points + mut pool_token_frac: u16, + mut fungible_staked_sui_data_total_supply_frac: u16, + mut fungible_staked_sui_data_principal_frac: u16, + mut fungible_staked_sui_value_bps: u16 + ) { + use std::u128::max; + + total_sui_amount = std::u64::max(total_sui_amount, 1); + + pool_token_frac = pool_token_frac % 10000; + fungible_staked_sui_data_total_supply_frac = fungible_staked_sui_data_total_supply_frac % 10000; + fungible_staked_sui_data_principal_frac = fungible_staked_sui_data_principal_frac % 10000; + fungible_staked_sui_value_bps = fungible_staked_sui_value_bps % 10000; + + + let total_pool_token_amount = max( + (total_sui_amount as u128) * (pool_token_frac as u128) / 10000, + 1 + ); + + let exchange_rate = PoolTokenExchangeRate { + sui_amount: total_sui_amount, + pool_token_amount: total_pool_token_amount as u64, + }; + + let fungible_staked_sui_data_total_supply = max( + total_pool_token_amount * (fungible_staked_sui_data_total_supply_frac as u128) / 10000, + 1 + ); + let fungible_staked_sui_value = fungible_staked_sui_data_total_supply + * (fungible_staked_sui_value_bps as u128) / 10000; + + let max_principal = get_sui_amount(&exchange_rate, fungible_staked_sui_data_total_supply as u64); + let fungible_staked_sui_data_principal_amount = max( + (max_principal as u128) * (fungible_staked_sui_data_principal_frac as u128) / 10000, + 1 + ); + + let (principal_amount, rewards_amount) = calculate_fungible_staked_sui_withdraw_amount( + exchange_rate, + fungible_staked_sui_value as u64, + fungible_staked_sui_data_principal_amount as u64, + fungible_staked_sui_data_total_supply as u64, + ); + + let expected_out = get_sui_amount(&exchange_rate, fungible_staked_sui_value as u64); + + assert!(principal_amount + rewards_amount <= expected_out, 0); + + let min_out = if (expected_out > 2) expected_out - 2 else 0; + assert!(principal_amount + rewards_amount >= min_out, 0); + } + + } diff --git a/crates/sui-framework/packages/sui-system/sources/sui_system.move b/crates/sui-framework/packages/sui-system/sources/sui_system.move index fe46fff592e6a..916c2cd55b33b 100644 --- a/crates/sui-framework/packages/sui-system/sources/sui_system.move +++ b/crates/sui-framework/packages/sui-system/sources/sui_system.move @@ -42,7 +42,7 @@ module sui_system::sui_system { use sui::balance::Balance; use sui::coin::Coin; - use sui_system::staking_pool::StakedSui; + use sui_system::staking_pool::{StakedSui, FungibleStakedSui}; use sui::sui::SUI; use sui::table::Table; use sui_system::validator::Validator; @@ -265,6 +265,26 @@ module sui_system::sui_system { transfer::public_transfer(withdrawn_stake.into_coin(ctx), ctx.sender()); } + /// Convert StakedSui into a FungibleStakedSui object. + public fun convert_to_fungible_staked_sui( + wrapper: &mut SuiSystemState, + staked_sui: StakedSui, + ctx: &mut TxContext, + ): FungibleStakedSui { + let self = load_system_state_mut(wrapper); + self.convert_to_fungible_staked_sui(staked_sui, ctx) + } + + /// Convert FungibleStakedSui into a StakedSui object. + public fun redeem_fungible_staked_sui( + wrapper: &mut SuiSystemState, + fungible_staked_sui: FungibleStakedSui, + ctx: &TxContext, + ): Balance { + let self = load_system_state_mut(wrapper); + self.redeem_fungible_staked_sui(fungible_staked_sui, ctx) + } + /// Non-entry version of `request_withdraw_stake` that returns the withdrawn SUI instead of transferring it to the sender. public fun request_withdraw_stake_non_entry( wrapper: &mut SuiSystemState, diff --git a/crates/sui-framework/packages/sui-system/sources/sui_system_state_inner.move b/crates/sui-framework/packages/sui-system/sources/sui_system_state_inner.move index ae23b97b4fd40..121a12fc75b94 100644 --- a/crates/sui-framework/packages/sui-system/sources/sui_system_state_inner.move +++ b/crates/sui-framework/packages/sui-system/sources/sui_system_state_inner.move @@ -4,7 +4,7 @@ module sui_system::sui_system_state_inner { use sui::balance::{Self, Balance}; use sui::coin::Coin; - use sui_system::staking_pool::StakedSui; + use sui_system::staking_pool::{StakedSui, FungibleStakedSui}; use sui::sui::SUI; use sui_system::validator::{Self, Validator}; use sui_system::validator_set::{Self, ValidatorSet}; @@ -518,6 +518,22 @@ module sui_system::sui_system_state_inner { self.validators.request_withdraw_stake(staked_sui, ctx) } + public(package) fun convert_to_fungible_staked_sui( + self: &mut SuiSystemStateInnerV2, + staked_sui: StakedSui, + ctx: &mut TxContext, + ) : FungibleStakedSui { + self.validators.convert_to_fungible_staked_sui(staked_sui, ctx) + } + + public(package) fun redeem_fungible_staked_sui( + self: &mut SuiSystemStateInnerV2, + fungible_staked_sui: FungibleStakedSui, + ctx: &TxContext, + ) : Balance { + self.validators.redeem_fungible_staked_sui(fungible_staked_sui, ctx) + } + /// Report a validator as a bad or non-performant actor in the system. /// Succeeds if all the following are satisfied: /// 1. both the reporter in `cap` and the input `reportee_addr` are active validators. diff --git a/crates/sui-framework/packages/sui-system/sources/validator.move b/crates/sui-framework/packages/sui-system/sources/validator.move index 5c0a453ea779f..b3f8e3d6f3a09 100644 --- a/crates/sui-framework/packages/sui-system/sources/validator.move +++ b/crates/sui-framework/packages/sui-system/sources/validator.move @@ -8,7 +8,7 @@ module sui_system::validator { use sui::balance::Balance; use sui::sui::SUI; use sui_system::validator_cap::{Self, ValidatorOperationCap}; - use sui_system::staking_pool::{Self, PoolTokenExchangeRate, StakedSui, StakingPool}; + use sui_system::staking_pool::{Self, PoolTokenExchangeRate, StakedSui, StakingPool, FungibleStakedSui}; use std::string::String; use sui::url::Url; use sui::url; @@ -306,6 +306,22 @@ module sui_system::validator { staked_sui } + public(package) fun convert_to_fungible_staked_sui( + self: &mut Validator, + staked_sui: StakedSui, + ctx: &mut TxContext, + ) : FungibleStakedSui { + self.staking_pool.convert_to_fungible_staked_sui(staked_sui, ctx) + } + + public(package) fun redeem_fungible_staked_sui( + self: &mut Validator, + fungible_staked_sui: FungibleStakedSui, + ctx: &TxContext, + ) : Balance { + self.staking_pool.redeem_fungible_staked_sui(fungible_staked_sui, ctx) + } + /// Request to add stake to the validator's staking pool at genesis public(package) fun request_add_stake_at_genesis( self: &mut Validator, diff --git a/crates/sui-framework/packages/sui-system/sources/validator_set.move b/crates/sui-framework/packages/sui-system/sources/validator_set.move index 2cc069f023b71..3f631642fa577 100644 --- a/crates/sui-framework/packages/sui-system/sources/validator_set.move +++ b/crates/sui-framework/packages/sui-system/sources/validator_set.move @@ -7,7 +7,7 @@ module sui_system::validator_set { use sui::sui::SUI; use sui_system::validator::{Validator, staking_pool_id, sui_address}; use sui_system::validator_cap::{Self, UnverifiedValidatorOperationCap, ValidatorOperationCap}; - use sui_system::staking_pool::{PoolTokenExchangeRate, StakedSui, pool_id}; + use sui_system::staking_pool::{PoolTokenExchangeRate, StakedSui, pool_id, FungibleStakedSui, fungible_staked_sui_pool_id}; use sui::priority_queue as pq; use sui::vec_map::{Self, VecMap}; use sui::vec_set::VecSet; @@ -309,6 +309,45 @@ module sui_system::validator_set { validator.request_withdraw_stake(staked_sui, ctx) } + public(package) fun convert_to_fungible_staked_sui( + self: &mut ValidatorSet, + staked_sui: StakedSui, + ctx: &mut TxContext, + ) : FungibleStakedSui { + let staking_pool_id = pool_id(&staked_sui); + let validator = + if (self.staking_pool_mappings.contains(staking_pool_id)) { // This is an active validator. + let validator_address = self.staking_pool_mappings[pool_id(&staked_sui)]; + get_candidate_or_active_validator_mut(self, validator_address) + } else { // This is an inactive pool. + assert!(self.inactive_validators.contains(staking_pool_id), ENoPoolFound); + let wrapper = &mut self.inactive_validators[staking_pool_id]; + wrapper.load_validator_maybe_upgrade() + }; + + validator.convert_to_fungible_staked_sui(staked_sui, ctx) + } + + public(package) fun redeem_fungible_staked_sui( + self: &mut ValidatorSet, + fungible_staked_sui: FungibleStakedSui, + ctx: &TxContext, + ) : Balance { + let staking_pool_id = fungible_staked_sui_pool_id(&fungible_staked_sui); + + let validator = + if (self.staking_pool_mappings.contains(staking_pool_id)) { // This is an active validator. + let validator_address = self.staking_pool_mappings[staking_pool_id]; + get_candidate_or_active_validator_mut(self, validator_address) + } else { // This is an inactive pool. + assert!(self.inactive_validators.contains(staking_pool_id), ENoPoolFound); + let wrapper = &mut self.inactive_validators[staking_pool_id]; + wrapper.load_validator_maybe_upgrade() + }; + + validator.redeem_fungible_staked_sui(fungible_staked_sui, ctx) + } + // ==== validator config setting functions ==== public(package) fun request_set_commission_rate( diff --git a/crates/sui-framework/packages/sui-system/tests/staking_pool.move b/crates/sui-framework/packages/sui-system/tests/staking_pool.move new file mode 100644 index 0000000000000..d5ed2cdef5599 --- /dev/null +++ b/crates/sui-framework/packages/sui-system/tests/staking_pool.move @@ -0,0 +1,302 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module sui_system::staking_pool_tests { + use sui::test_scenario::{Self, Scenario}; + use sui_system::staking_pool::{StakingPool, Self}; + use sui::balance::{Self}; + + #[test] + fun test_join_fungible_staked_sui_happy() { + let mut scenario = test_scenario::begin(@0x0); + let staking_pool = staking_pool::new(scenario.ctx()); + + let mut fungible_staked_sui_1 = staking_pool.create_fungible_staked_sui_for_testing(100_000_000_000, scenario.ctx()); + let fungible_staked_sui_2 = staking_pool.create_fungible_staked_sui_for_testing(200_000_000_000, scenario.ctx()); + + fungible_staked_sui_1.join(fungible_staked_sui_2); + + assert!(fungible_staked_sui_1.value() == 300_000_000_000, 0); + + sui::test_utils::destroy(staking_pool); + sui::test_utils::destroy(fungible_staked_sui_1); + + scenario.end(); + } + + #[test] + #[expected_failure(abort_code = 1, location = sui_system::staking_pool)] + fun test_join_fungible_staked_sui_fail() { + let mut scenario = test_scenario::begin(@0x0); + let staking_pool_1 = staking_pool::new(scenario.ctx()); + let staking_pool_2 = staking_pool::new(scenario.ctx()); + + let mut fungible_staked_sui_1 = staking_pool_1.create_fungible_staked_sui_for_testing(100_000_000_000, scenario.ctx()); + let fungible_staked_sui_2 = staking_pool_2.create_fungible_staked_sui_for_testing(200_000_000_000, scenario.ctx()); + + fungible_staked_sui_1.join(fungible_staked_sui_2); + + sui::test_utils::destroy(staking_pool_1); + sui::test_utils::destroy(staking_pool_2); + sui::test_utils::destroy(fungible_staked_sui_1); + + scenario.end(); + } + + #[test] + fun test_split_fungible_staked_sui_happy() { + let mut scenario = test_scenario::begin(@0x0); + let staking_pool = staking_pool::new(scenario.ctx()); + + let mut fungible_staked_sui_1 = staking_pool.create_fungible_staked_sui_for_testing(100_000_000_000, scenario.ctx()); + + let fungible_staked_sui_2 = fungible_staked_sui_1.split(75_000_000_000, scenario.ctx()); + + assert!(fungible_staked_sui_1.value() == 25_000_000_000, 0); + assert!(fungible_staked_sui_2.value() == 75_000_000_000, 0); + + sui::test_utils::destroy(staking_pool); + sui::test_utils::destroy(fungible_staked_sui_1); + sui::test_utils::destroy(fungible_staked_sui_2); + + scenario.end(); + } + + #[test] + #[expected_failure(abort_code = 18, location = sui_system::staking_pool)] + fun test_split_fungible_staked_sui_fail_too_little_1() { + let mut scenario = test_scenario::begin(@0x0); + let staking_pool = staking_pool::new(scenario.ctx()); + + let mut fungible_staked_sui_1 = staking_pool.create_fungible_staked_sui_for_testing(100_000_000_000, scenario.ctx()); + + let fungible_staked_sui_2 = fungible_staked_sui_1.split(100_000_000_000 - 1, scenario.ctx()); + + sui::test_utils::destroy(staking_pool); + sui::test_utils::destroy(fungible_staked_sui_1); + sui::test_utils::destroy(fungible_staked_sui_2); + + scenario.end(); + } + + #[test] + #[expected_failure(abort_code = 18, location = sui_system::staking_pool)] + fun test_split_fungible_staked_sui_fail_too_little_2() { + let mut scenario = test_scenario::begin(@0x0); + let staking_pool = staking_pool::new(scenario.ctx()); + + let mut fungible_staked_sui_1 = staking_pool.create_fungible_staked_sui_for_testing(100_000_000_000, scenario.ctx()); + + let fungible_staked_sui_2 = fungible_staked_sui_1.split(1, scenario.ctx()); + + sui::test_utils::destroy(staking_pool); + sui::test_utils::destroy(fungible_staked_sui_1); + sui::test_utils::destroy(fungible_staked_sui_2); + + scenario.end(); + } + + #[test] + #[expected_failure(abort_code = 0, location = sui_system::staking_pool)] + fun test_split_fungible_staked_sui_fail_too_much() { + let mut scenario = test_scenario::begin(@0x0); + let staking_pool = staking_pool::new(scenario.ctx()); + + let mut fungible_staked_sui_1 = staking_pool.create_fungible_staked_sui_for_testing(100_000_000_000, scenario.ctx()); + + let fungible_staked_sui_2 = fungible_staked_sui_1.split(100_000_000_000 + 1, scenario.ctx()); + + sui::test_utils::destroy(staking_pool); + sui::test_utils::destroy(fungible_staked_sui_1); + sui::test_utils::destroy(fungible_staked_sui_2); + + scenario.end(); + } + + #[test] + #[expected_failure(abort_code = 19, location = sui_system::staking_pool)] + fun test_convert_to_fungible_staked_sui_fail_too_early() { + let mut scenario = test_scenario::begin(@0x0); + let mut staking_pool = staking_pool::new(scenario.ctx()); + + let sui = balance::create_for_testing(1_000_000_000); + let staked_sui = staking_pool.request_add_stake(sui, scenario.ctx().epoch() + 1, scenario.ctx()); + let fungible_staked_sui = staking_pool.convert_to_fungible_staked_sui(staked_sui, scenario.ctx()); + + sui::test_utils::destroy(staking_pool); + sui::test_utils::destroy(fungible_staked_sui); + + scenario.end(); + } + + #[test] + #[expected_failure(abort_code = 1, location = sui_system::staking_pool)] + fun test_convert_to_fungible_staked_sui_fail_wrong_pool() { + let mut scenario = test_scenario::begin(@0x0); + let mut staking_pool_1 = staking_pool::new(scenario.ctx()); + let mut staking_pool_2 = staking_pool::new(scenario.ctx()); + + let sui = balance::create_for_testing(1_000_000_000); + let staked_sui = staking_pool_1.request_add_stake(sui, scenario.ctx().epoch() + 1, scenario.ctx()); + + let fungible_staked_sui = staking_pool_2.convert_to_fungible_staked_sui(staked_sui, scenario.ctx()); + + sui::test_utils::destroy(staking_pool_1); + sui::test_utils::destroy(staking_pool_2); + sui::test_utils::destroy(fungible_staked_sui); + + scenario.end(); + } + + #[test] + fun test_convert_to_fungible_staked_sui_happy() { + let mut scenario = test_scenario::begin(@0x0); + let mut staking_pool = staking_pool::new(scenario.ctx()); + staking_pool.activate_staking_pool(0); + + // setup + + let sui = balance::create_for_testing(1_000_000_000); + let staked_sui_1 = staking_pool.request_add_stake(sui, scenario.ctx().epoch() + 1, scenario.ctx()); + + assert!(distribute_rewards_and_advance_epoch(&mut staking_pool, &mut scenario, 0) == 1, 0); + + let latest_exchange_rate = staking_pool.pool_token_exchange_rate_at_epoch(1); + assert!(latest_exchange_rate.sui_amount() == 1_000_000_000, 0); + assert!(latest_exchange_rate.pool_token_amount() == 1_000_000_000, 0); + + let sui = balance::create_for_testing(1_000_000_000); + let staked_sui_2 = staking_pool.request_add_stake(sui, scenario.ctx().epoch() + 1, scenario.ctx()); + + assert!(distribute_rewards_and_advance_epoch(&mut staking_pool, &mut scenario, 1_000_000_000) == 2, 0); + + let latest_exchange_rate = staking_pool.pool_token_exchange_rate_at_epoch(2); + assert!(latest_exchange_rate.sui_amount() == 3_000_000_000, 0); + assert!(latest_exchange_rate.pool_token_amount() == 1_500_000_000, 0); + + // test basically starts from here. + + let fungible_staked_sui_1 = staking_pool.convert_to_fungible_staked_sui(staked_sui_1, scenario.ctx()); + assert!(fungible_staked_sui_1.value() == 1_000_000_000, 0); + assert!(fungible_staked_sui_1.pool_id() == object::id(&staking_pool), 0); + + let fungible_staked_sui_data = staking_pool.fungible_staked_sui_data(); + assert!(fungible_staked_sui_data.total_supply() == 1_000_000_000, 0); + assert!(fungible_staked_sui_data.principal_value() == 1_000_000_000, 0); + + let fungible_staked_sui_2 = staking_pool.convert_to_fungible_staked_sui(staked_sui_2, scenario.ctx()); + assert!(fungible_staked_sui_2.value() == 500_000_000, 0); + assert!(fungible_staked_sui_2.pool_id() == object::id(&staking_pool), 0); + + let fungible_staked_sui_data = staking_pool.fungible_staked_sui_data(); + assert!(fungible_staked_sui_data.total_supply() == 1_500_000_000, 0); + assert!(fungible_staked_sui_data.principal_value() == 2_000_000_000, 0); + + sui::test_utils::destroy(staking_pool); + // sui::test_utils::destroy(fungible_staked_sui); + sui::test_utils::destroy(fungible_staked_sui_1); + sui::test_utils::destroy(fungible_staked_sui_2); + + scenario.end(); + } + + #[test] + fun test_redeem_fungible_staked_sui_happy() { + let mut scenario = test_scenario::begin(@0x0); + let mut staking_pool = staking_pool::new(scenario.ctx()); + staking_pool.activate_staking_pool(0); + + // setup + + let sui = balance::create_for_testing(1_000_000_000); + let staked_sui_1 = staking_pool.request_add_stake(sui, scenario.ctx().epoch() + 1, scenario.ctx()); + + assert!(distribute_rewards_and_advance_epoch(&mut staking_pool, &mut scenario, 0) == 1, 0); + + let latest_exchange_rate = staking_pool.pool_token_exchange_rate_at_epoch(1); + assert!(latest_exchange_rate.sui_amount() == 1_000_000_000, 0); + assert!(latest_exchange_rate.pool_token_amount() == 1_000_000_000, 0); + + let sui = balance::create_for_testing(1_000_000_000); + let staked_sui_2 = staking_pool.request_add_stake(sui, scenario.ctx().epoch() + 1, scenario.ctx()); + + assert!(distribute_rewards_and_advance_epoch(&mut staking_pool, &mut scenario, 1_000_000_000) == 2, 0); + + let latest_exchange_rate = staking_pool.pool_token_exchange_rate_at_epoch(2); + assert!(latest_exchange_rate.sui_amount() == 3_000_000_000, 0); + assert!(latest_exchange_rate.pool_token_amount() == 1_500_000_000, 0); + + let fungible_staked_sui_1 = staking_pool.convert_to_fungible_staked_sui(staked_sui_1, scenario.ctx()); + assert!(fungible_staked_sui_1.value() == 1_000_000_000, 0); + assert!(fungible_staked_sui_1.pool_id() == object::id(&staking_pool), 0); + + let fungible_staked_sui_data = staking_pool.fungible_staked_sui_data(); + assert!(fungible_staked_sui_data.total_supply() == 1_000_000_000, 0); + assert!(fungible_staked_sui_data.principal_value() == 1_000_000_000, 0); + + let fungible_staked_sui_2 = staking_pool.convert_to_fungible_staked_sui(staked_sui_2, scenario.ctx()); + assert!(fungible_staked_sui_2.value() == 500_000_000, 0); + assert!(fungible_staked_sui_2.pool_id() == object::id(&staking_pool), 0); + + let fungible_staked_sui_data = staking_pool.fungible_staked_sui_data(); + assert!(fungible_staked_sui_data.total_supply() == 1_500_000_000, 0); + assert!(fungible_staked_sui_data.principal_value() == 2_000_000_000, 0); + + // test starts here + assert!(distribute_rewards_and_advance_epoch(&mut staking_pool, &mut scenario, 3_000_000_000) == 3, 0); + + let latest_exchange_rate = staking_pool.pool_token_exchange_rate_at_epoch(3); + assert!(latest_exchange_rate.sui_amount() == 6_000_000_000, 0); + assert!(latest_exchange_rate.pool_token_amount() == 1_500_000_000, 0); + + assert!(staking_pool.pending_stake_withdraw_amount() == 0, 0); + assert!(staking_pool.pending_pool_token_withdraw_amount() == 0, 0); + + let sui_1 = staking_pool.redeem_fungible_staked_sui(fungible_staked_sui_1, scenario.ctx()); + assert!(sui_1.value() <= 4_000_000_000, 0); + assert!(sui_1.value() == 4_000_000_000 - 1, 0); + + let fungible_staked_sui_data = staking_pool.fungible_staked_sui_data(); + assert!(fungible_staked_sui_data.total_supply() == 500_000_000, 0); + assert!(fungible_staked_sui_data.principal_value() == 2_000_000_000 / 3 + 1, 0); // round against user + + assert!(staking_pool.pending_stake_withdraw_amount() == 4_000_000_000 - 1, 0); + assert!(staking_pool.pending_pool_token_withdraw_amount() == 1_000_000_000, 0); + + let sui_2 = staking_pool.redeem_fungible_staked_sui(fungible_staked_sui_2, scenario.ctx()); + assert!(sui_2.value() == 2_000_000_000, 0); + + let fungible_staked_sui_data = staking_pool.fungible_staked_sui_data(); + assert!(fungible_staked_sui_data.total_supply() == 0, 0); + assert!(fungible_staked_sui_data.principal_value() == 0, 0); + + assert!(staking_pool.pending_stake_withdraw_amount() == 6_000_000_000 - 1, 0); + assert!(staking_pool.pending_pool_token_withdraw_amount() == 1_500_000_000, 0); + + sui::test_utils::destroy(staking_pool); + sui::test_utils::destroy(sui_1); + sui::test_utils::destroy(sui_2); + + scenario.end(); + } + + #[test_only] + fun distribute_rewards_and_advance_epoch( + staking_pool: &mut StakingPool, + scenario: &mut Scenario, + reward_amount: u64 + ): u64 { + use sui::tx_context::{epoch}; + use sui::coin::{Self}; + use sui::sui::SUI; + + let rewards = coin::mint_for_testing(reward_amount, scenario.ctx()); + staking_pool.deposit_rewards(coin::into_balance(rewards)); + + staking_pool.process_pending_stakes_and_withdraws(scenario.ctx()); + test_scenario::next_epoch(scenario, @0x0); + + scenario.ctx().epoch() + } +} diff --git a/crates/sui-framework/packages/sui-system/tests/sui_system_tests.move b/crates/sui-framework/packages/sui-system/tests/sui_system_tests.move index 25c963d2aa9ea..d333f4e64a642 100644 --- a/crates/sui-framework/packages/sui-system/tests/sui_system_tests.move +++ b/crates/sui-framework/packages/sui-system/tests/sui_system_tests.move @@ -9,6 +9,7 @@ module sui_system::sui_system_tests { use sui::test_scenario::{Self, Scenario}; use sui::sui::SUI; + use sui::coin::Self; use sui_system::governance_test_utils::{add_validator_full_flow, advance_epoch, remove_validator, set_up_sui_system_state, create_sui_system_state_for_testing, stake_with, unstake}; use sui_system::sui_system::SuiSystemState; use sui_system::sui_system_state_inner; @@ -1070,4 +1071,58 @@ module sui_system::sui_system_tests { scenario_val.end(); } + + #[test] + fun test_convert_to_fungible_staked_sui_and_redeem() { + let mut scenario_val = test_scenario::begin(@0x0); + let scenario = &mut scenario_val; + // Epoch duration is set to be 42 here. + set_up_sui_system_state(vector[@0x1, @0x2]); + + { + scenario.next_tx(@0x0); + let mut system_state = scenario.take_shared(); + let staking_pool = system_state.active_validator_by_address(@0x1).get_staking_pool_ref(); + + assert!(staking_pool.pending_stake_amount() == 0, 0); + assert!(staking_pool.pending_stake_withdraw_amount() == 0, 0); + assert!(staking_pool.sui_balance() == 100 * 1_000_000_000, 0); + + test_scenario::return_shared(system_state); + }; + + scenario.next_tx(@0x0); + let mut system_state = scenario.take_shared(); + + let staked_sui = system_state.request_add_stake_non_entry( + coin::mint_for_testing(100_000_000_000, scenario.ctx()), + @0x1, + scenario.ctx() + ); + + assert!(staked_sui.amount() == 100_000_000_000, 0); + + test_scenario::return_shared(system_state); + advance_epoch(scenario); + + let mut system_state = scenario.take_shared(); + let fungible_staked_sui = system_state.convert_to_fungible_staked_sui( + staked_sui, + scenario.ctx() + ); + + assert!(fungible_staked_sui.value() == 100_000_000_000, 0); + + let sui = system_state.redeem_fungible_staked_sui( + fungible_staked_sui, + scenario.ctx() + ); + + assert!(sui.value() == 100_000_000_000, 0); + + test_scenario::return_shared(system_state); + sui::test_utils::destroy(sui); + scenario_val.end(); + } + } From 6e9c430696ffd472ca91549271eb5ab0d1914d18 Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Mon, 29 Jul 2024 10:30:40 -0400 Subject: [PATCH 02/10] add test only to the method functions --- .../sui-framework/packages/sui-system/sources/staking_pool.move | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/sui-framework/packages/sui-system/sources/staking_pool.move b/crates/sui-framework/packages/sui-system/sources/staking_pool.move index 774cdeaf86ff1..52cb5596bbe8e 100644 --- a/crates/sui-framework/packages/sui-system/sources/staking_pool.move +++ b/crates/sui-framework/packages/sui-system/sources/staking_pool.move @@ -662,6 +662,7 @@ module sui_system::staking_pool { bag::borrow(&pool.extra_fields, FungibleStakedSuiDataKey {}) } + #[test_only] public use fun fungible_staked_sui_data_total_supply as FungibleStakedSuiData.total_supply; #[test_only] @@ -669,6 +670,7 @@ module sui_system::staking_pool { fungible_staked_sui_data.total_supply } + #[test_only] public use fun fungible_staked_sui_data_principal_value as FungibleStakedSuiData.principal_value; #[test_only] From d68ab769b4813a9c39808ef639aab18e882f3fdc Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Tue, 30 Jul 2024 13:11:38 -0400 Subject: [PATCH 03/10] fix docs --- .../docs/sui-system/staking_pool.md | 431 ++++++++++++++++++ .../docs/sui-system/sui_system.md | 62 +++ .../docs/sui-system/sui_system_state_inner.md | 58 +++ .../docs/sui-system/validator.md | 58 +++ .../docs/sui-system/validator_set.md | 81 ++++ .../packages_compiled/sui-system | Bin 41971 -> 43984 bytes crates/sui-framework/published_api.txt | 54 +++ 7 files changed, 744 insertions(+) diff --git a/crates/sui-framework/docs/sui-system/staking_pool.md b/crates/sui-framework/docs/sui-system/staking_pool.md index 782ad81ed3cc1..747c82c18161a 100644 --- a/crates/sui-framework/docs/sui-system/staking_pool.md +++ b/crates/sui-framework/docs/sui-system/staking_pool.md @@ -7,10 +7,16 @@ title: Module `0x3::staking_pool` - [Resource `StakingPool`](#0x3_staking_pool_StakingPool) - [Struct `PoolTokenExchangeRate`](#0x3_staking_pool_PoolTokenExchangeRate) - [Resource `StakedSui`](#0x3_staking_pool_StakedSui) +- [Resource `FungibleStakedSui`](#0x3_staking_pool_FungibleStakedSui) +- [Resource `FungibleStakedSuiData`](#0x3_staking_pool_FungibleStakedSuiData) +- [Struct `FungibleStakedSuiDataKey`](#0x3_staking_pool_FungibleStakedSuiDataKey) - [Constants](#@Constants_0) - [Function `new`](#0x3_staking_pool_new) - [Function `request_add_stake`](#0x3_staking_pool_request_add_stake) - [Function `request_withdraw_stake`](#0x3_staking_pool_request_withdraw_stake) +- [Function `redeem_fungible_staked_sui`](#0x3_staking_pool_redeem_fungible_staked_sui) +- [Function `calculate_fungible_staked_sui_withdraw_amount`](#0x3_staking_pool_calculate_fungible_staked_sui_withdraw_amount) +- [Function `convert_to_fungible_staked_sui`](#0x3_staking_pool_convert_to_fungible_staked_sui) - [Function `withdraw_from_principal`](#0x3_staking_pool_withdraw_from_principal) - [Function `unwrap_staked_sui`](#0x3_staking_pool_unwrap_staked_sui) - [Function `deposit_rewards`](#0x3_staking_pool_deposit_rewards) @@ -22,10 +28,14 @@ title: Module `0x3::staking_pool` - [Function `deactivate_staking_pool`](#0x3_staking_pool_deactivate_staking_pool) - [Function `sui_balance`](#0x3_staking_pool_sui_balance) - [Function `pool_id`](#0x3_staking_pool_pool_id) +- [Function `fungible_staked_sui_pool_id`](#0x3_staking_pool_fungible_staked_sui_pool_id) - [Function `staked_sui_amount`](#0x3_staking_pool_staked_sui_amount) - [Function `stake_activation_epoch`](#0x3_staking_pool_stake_activation_epoch) - [Function `is_preactive`](#0x3_staking_pool_is_preactive) - [Function `is_inactive`](#0x3_staking_pool_is_inactive) +- [Function `fungible_staked_sui_value`](#0x3_staking_pool_fungible_staked_sui_value) +- [Function `split_fungible_staked_sui`](#0x3_staking_pool_split_fungible_staked_sui) +- [Function `join_fungible_staked_sui`](#0x3_staking_pool_join_fungible_staked_sui) - [Function `split`](#0x3_staking_pool_split) - [Function `split_staked_sui`](#0x3_staking_pool_split_staked_sui) - [Function `join_staked_sui`](#0x3_staking_pool_join_staked_sui) @@ -228,6 +238,116 @@ A self-custodial object holding the staked SUI tokens. + + + + +## Resource `FungibleStakedSui` + +An alternative to StakedSui that holds the pool token amount instead of the SUI balance. +StakedSui objects can be converted to FungibleStakedSuis after the initial warmup period. +The advantage of this is that you can now merge multiple StakedSui objects from different +activation epochs into a single FungibleStakedSui object. + + +
struct FungibleStakedSui has store, key
+
+ + + +
+Fields + + +
+
+id: object::UID +
+
+ +
+
+pool_id: object::ID +
+
+ ID of the staking pool we are staking with. +
+
+value: u64 +
+
+ The pool token amount. +
+
+ + +
+ + + +## Resource `FungibleStakedSuiData` + +Holds useful information + + +
struct FungibleStakedSuiData has store, key
+
+ + + +
+Fields + + +
+
+id: object::UID +
+
+ +
+
+total_supply: u64 +
+
+ fungible_staked_sui supply +
+
+principal: balance::Balance<sui::SUI> +
+
+ principal balance. Rewards are withdrawn from the reward pool +
+
+ + +
+ + + +## Struct `FungibleStakedSuiDataKey` + + + +
struct FungibleStakedSuiDataKey has copy, drop, store
+
+ + + +
+Fields + + +
+
+dummy_field: bool +
+
+ +
+
+ +
@@ -244,6 +364,15 @@ A self-custodial object holding the staked SUI tokens. + + + + +
const ECannotMintFungibleStakedSuiYet: u64 = 19;
+
+ + + @@ -316,6 +445,15 @@ A self-custodial object holding the staked SUI tokens. + + + + +
const EInvariantFailure: u64 = 20;
+
+ + + @@ -548,6 +686,187 @@ A proportional amount of pool token withdraw is recorded and processed at epoch + + + + +## Function `redeem_fungible_staked_sui` + + + +
public(friend) fun redeem_fungible_staked_sui(pool: &mut staking_pool::StakingPool, fungible_staked_sui: staking_pool::FungibleStakedSui, ctx: &tx_context::TxContext): balance::Balance<sui::SUI>
+
+ + + +
+Implementation + + +
public(package) fun redeem_fungible_staked_sui(
+    pool: &mut StakingPool,
+    fungible_staked_sui: FungibleStakedSui,
+    ctx: &TxContext
+) : Balance<SUI> {
+    let FungibleStakedSui { id, pool_id, value } = fungible_staked_sui;
+    assert!(pool_id == object::id(pool), EWrongPool);
+
+    object::delete(id);
+
+    let latest_exchange_rate = pool_token_exchange_rate_at_epoch(pool, tx_context::epoch(ctx));
+    let fungible_staked_sui_data: &mut FungibleStakedSuiData = bag::borrow_mut(
+        &mut pool.extra_fields,
+        FungibleStakedSuiDataKey {}
+    );
+
+    let (principal_amount, rewards_amount) = calculate_fungible_staked_sui_withdraw_amount(
+        latest_exchange_rate,
+        value,
+        balance::value(&fungible_staked_sui_data.principal),
+        fungible_staked_sui_data.total_supply
+    );
+
+    fungible_staked_sui_data.total_supply = fungible_staked_sui_data.total_supply - value;
+
+    let mut sui_out = balance::split(&mut fungible_staked_sui_data.principal, principal_amount);
+    balance::join(
+        &mut sui_out,
+        balance::split(&mut pool.rewards_pool, rewards_amount)
+    );
+
+    pool.pending_total_sui_withdraw = pool.pending_total_sui_withdraw + balance::value(&sui_out);
+    pool.pending_pool_token_withdraw = pool.pending_pool_token_withdraw + value;
+
+    sui_out
+}
+
+ + + +
+ + + +## Function `calculate_fungible_staked_sui_withdraw_amount` + +written in separate function so i can test with random values +returns (principal_withdraw_amount, rewards_withdraw_amount) + + +
fun calculate_fungible_staked_sui_withdraw_amount(latest_exchange_rate: staking_pool::PoolTokenExchangeRate, fungible_staked_sui_value: u64, fungible_staked_sui_data_principal_amount: u64, fungible_staked_sui_data_total_supply: u64): (u64, u64)
+
+ + + +
+Implementation + + +
fun calculate_fungible_staked_sui_withdraw_amount(
+    latest_exchange_rate: PoolTokenExchangeRate,
+    fungible_staked_sui_value: u64,
+    fungible_staked_sui_data_principal_amount: u64, // fungible_staked_sui_data.principal.value()
+    fungible_staked_sui_data_total_supply: u64, // fungible_staked_sui_data.total_supply
+) : (u64, u64) {
+    // 1. if the entire FungibleStakedSuiData supply is redeemed, how much sui should we receive?
+    let total_sui_amount = get_sui_amount(&latest_exchange_rate, fungible_staked_sui_data_total_supply);
+
+    // 2. how much do we need to withdraw from the rewards pool?
+    let total_rewards = total_sui_amount - fungible_staked_sui_data_principal_amount;
+
+    // 3. proportionally withdraw from both wrt the fungible_staked_sui_value.
+    let principal_withdraw_amount = ((fungible_staked_sui_value as u128)
+        * (fungible_staked_sui_data_principal_amount as u128)
+        / (fungible_staked_sui_data_total_supply as u128)) as u64;
+
+    let rewards_withdraw_amount = ((fungible_staked_sui_value as u128)
+        * (total_rewards as u128)
+        / (fungible_staked_sui_data_total_supply as u128)) as u64;
+
+    // invariant check, just in case
+    let expected_sui_amount = get_sui_amount(&latest_exchange_rate, fungible_staked_sui_value);
+    assert!(principal_withdraw_amount + rewards_withdraw_amount <= expected_sui_amount, EInvariantFailure);
+
+    (principal_withdraw_amount, rewards_withdraw_amount)
+}
+
+ + + +
+ + + +## Function `convert_to_fungible_staked_sui` + +Convert the given staked SUI to an FungibleStakedSui object + + +
public(friend) fun convert_to_fungible_staked_sui(pool: &mut staking_pool::StakingPool, staked_sui: staking_pool::StakedSui, ctx: &mut tx_context::TxContext): staking_pool::FungibleStakedSui
+
+ + + +
+Implementation + + +
public(package) fun convert_to_fungible_staked_sui(
+    pool: &mut StakingPool,
+    staked_sui: StakedSui,
+    ctx: &mut TxContext
+) : FungibleStakedSui {
+    let StakedSui { id, pool_id, stake_activation_epoch, principal } = staked_sui;
+
+    assert!(pool_id == object::id(pool), EWrongPool);
+    assert!(
+        tx_context::epoch(ctx) >= stake_activation_epoch,
+        ECannotMintFungibleStakedSuiYet
+    );
+
+    object::delete(id);
+
+
+    let exchange_rate_at_staking_epoch = pool_token_exchange_rate_at_epoch(
+        pool,
+        stake_activation_epoch
+    );
+
+    let pool_token_amount = get_token_amount(
+        &exchange_rate_at_staking_epoch,
+        balance::value(&principal)
+    );
+
+    if (!bag::contains(&pool.extra_fields, FungibleStakedSuiDataKey {})) {
+        bag::add(
+            &mut pool.extra_fields,
+            FungibleStakedSuiDataKey {},
+            FungibleStakedSuiData {
+                id: object::new(ctx),
+                total_supply: pool_token_amount,
+                principal
+            }
+        );
+    }
+    else {
+        let fungible_staked_sui_data: &mut FungibleStakedSuiData = bag::borrow_mut(
+            &mut pool.extra_fields,
+            FungibleStakedSuiDataKey {}
+        );
+        fungible_staked_sui_data.total_supply = fungible_staked_sui_data.total_supply + pool_token_amount;
+        balance::join(&mut fungible_staked_sui_data.principal, principal);
+    };
+
+    FungibleStakedSui {
+        id: object::new(ctx),
+        pool_id,
+        value: pool_token_amount,
+    }
+}
+
+ + +
@@ -892,6 +1211,28 @@ withdraws can be made to the pool. + + + + +## Function `fungible_staked_sui_pool_id` + + + +
public fun fungible_staked_sui_pool_id(fungible_staked_sui: &staking_pool::FungibleStakedSui): object::ID
+
+ + + +
+Implementation + + +
public fun fungible_staked_sui_pool_id(fungible_staked_sui: &FungibleStakedSui): ID { fungible_staked_sui.pool_id }
+
+ + +
@@ -988,6 +1329,96 @@ Returns true if the input staking pool is inactive. + + + + +## Function `fungible_staked_sui_value` + + + +
public fun fungible_staked_sui_value(fungible_staked_sui: &staking_pool::FungibleStakedSui): u64
+
+ + + +
+Implementation + + +
public fun fungible_staked_sui_value(fungible_staked_sui: &FungibleStakedSui): u64 { fungible_staked_sui.value }
+
+ + + +
+ + + +## Function `split_fungible_staked_sui` + + + +
public fun split_fungible_staked_sui(fungible_staked_sui: &mut staking_pool::FungibleStakedSui, split_amount: u64, ctx: &mut tx_context::TxContext): staking_pool::FungibleStakedSui
+
+ + + +
+Implementation + + +
public fun split_fungible_staked_sui(
+    fungible_staked_sui: &mut FungibleStakedSui,
+    split_amount: u64,
+    ctx: &mut TxContext
+): FungibleStakedSui {
+    assert!(split_amount <= fungible_staked_sui.value, EInsufficientPoolTokenBalance);
+
+    fungible_staked_sui.value = fungible_staked_sui.value - split_amount;
+
+    assert!(fungible_staked_sui.value >= MIN_STAKING_THRESHOLD, EStakedSuiBelowThreshold);
+    assert!(split_amount >= MIN_STAKING_THRESHOLD, EStakedSuiBelowThreshold);
+
+    FungibleStakedSui {
+        id: object::new(ctx),
+        pool_id: fungible_staked_sui.pool_id,
+        value: split_amount,
+    }
+}
+
+ + + +
+ + + +## Function `join_fungible_staked_sui` + + + +
public fun join_fungible_staked_sui(self: &mut staking_pool::FungibleStakedSui, other: staking_pool::FungibleStakedSui)
+
+ + + +
+Implementation + + +
public fun join_fungible_staked_sui(self: &mut FungibleStakedSui, other: FungibleStakedSui) {
+    let FungibleStakedSui { id, pool_id, value } = other;
+    assert!(self.pool_id == pool_id, EWrongPool);
+
+    object::delete(id);
+
+    self.value = self.value + value;
+}
+
+ + +
diff --git a/crates/sui-framework/docs/sui-system/sui_system.md b/crates/sui-framework/docs/sui-system/sui_system.md index 9799ac326b842..ca151bd20b68c 100644 --- a/crates/sui-framework/docs/sui-system/sui_system.md +++ b/crates/sui-framework/docs/sui-system/sui_system.md @@ -55,6 +55,8 @@ the SuiSystemStateInner version, or vice versa. - [Function `request_add_stake_non_entry`](#0x3_sui_system_request_add_stake_non_entry) - [Function `request_add_stake_mul_coin`](#0x3_sui_system_request_add_stake_mul_coin) - [Function `request_withdraw_stake`](#0x3_sui_system_request_withdraw_stake) +- [Function `convert_to_fungible_staked_sui`](#0x3_sui_system_convert_to_fungible_staked_sui) +- [Function `redeem_fungible_staked_sui`](#0x3_sui_system_redeem_fungible_staked_sui) - [Function `request_withdraw_stake_non_entry`](#0x3_sui_system_request_withdraw_stake_non_entry) - [Function `report_validator`](#0x3_sui_system_report_validator) - [Function `undo_report_validator`](#0x3_sui_system_undo_report_validator) @@ -616,6 +618,66 @@ Withdraw stake from a validator's staking pool. + + + + +## Function `convert_to_fungible_staked_sui` + +Convert StakedSui into a FungibleStakedSui object. + + +
public fun convert_to_fungible_staked_sui(wrapper: &mut sui_system::SuiSystemState, staked_sui: staking_pool::StakedSui, ctx: &mut tx_context::TxContext): staking_pool::FungibleStakedSui
+
+ + + +
+Implementation + + +
public fun convert_to_fungible_staked_sui(
+    wrapper: &mut SuiSystemState,
+    staked_sui: StakedSui,
+    ctx: &mut TxContext,
+): FungibleStakedSui {
+    let self = load_system_state_mut(wrapper);
+    self.convert_to_fungible_staked_sui(staked_sui, ctx)
+}
+
+ + + +
+ + + +## Function `redeem_fungible_staked_sui` + +Convert FungibleStakedSui into a StakedSui object. + + +
public fun redeem_fungible_staked_sui(wrapper: &mut sui_system::SuiSystemState, fungible_staked_sui: staking_pool::FungibleStakedSui, ctx: &tx_context::TxContext): balance::Balance<sui::SUI>
+
+ + + +
+Implementation + + +
public fun redeem_fungible_staked_sui(
+    wrapper: &mut SuiSystemState,
+    fungible_staked_sui: FungibleStakedSui,
+    ctx: &TxContext,
+): Balance<SUI> {
+    let self = load_system_state_mut(wrapper);
+    self.redeem_fungible_staked_sui(fungible_staked_sui, ctx)
+}
+
+ + +
diff --git a/crates/sui-framework/docs/sui-system/sui_system_state_inner.md b/crates/sui-framework/docs/sui-system/sui_system_state_inner.md index c35887139f668..f4357743de41d 100644 --- a/crates/sui-framework/docs/sui-system/sui_system_state_inner.md +++ b/crates/sui-framework/docs/sui-system/sui_system_state_inner.md @@ -24,6 +24,8 @@ title: Module `0x3::sui_system_state_inner` - [Function `request_add_stake`](#0x3_sui_system_state_inner_request_add_stake) - [Function `request_add_stake_mul_coin`](#0x3_sui_system_state_inner_request_add_stake_mul_coin) - [Function `request_withdraw_stake`](#0x3_sui_system_state_inner_request_withdraw_stake) +- [Function `convert_to_fungible_staked_sui`](#0x3_sui_system_state_inner_convert_to_fungible_staked_sui) +- [Function `redeem_fungible_staked_sui`](#0x3_sui_system_state_inner_redeem_fungible_staked_sui) - [Function `report_validator`](#0x3_sui_system_state_inner_report_validator) - [Function `undo_report_validator`](#0x3_sui_system_state_inner_undo_report_validator) - [Function `report_validator_impl`](#0x3_sui_system_state_inner_report_validator_impl) @@ -1300,6 +1302,62 @@ Withdraw some portion of a stake from a validator's staking pool. + + + + +## Function `convert_to_fungible_staked_sui` + + + +
public(friend) fun convert_to_fungible_staked_sui(self: &mut sui_system_state_inner::SuiSystemStateInnerV2, staked_sui: staking_pool::StakedSui, ctx: &mut tx_context::TxContext): staking_pool::FungibleStakedSui
+
+ + + +
+Implementation + + +
public(package) fun convert_to_fungible_staked_sui(
+    self: &mut SuiSystemStateInnerV2,
+    staked_sui: StakedSui,
+    ctx: &mut TxContext,
+) : FungibleStakedSui {
+    self.validators.convert_to_fungible_staked_sui(staked_sui, ctx)
+}
+
+ + + +
+ + + +## Function `redeem_fungible_staked_sui` + + + +
public(friend) fun redeem_fungible_staked_sui(self: &mut sui_system_state_inner::SuiSystemStateInnerV2, fungible_staked_sui: staking_pool::FungibleStakedSui, ctx: &tx_context::TxContext): balance::Balance<sui::SUI>
+
+ + + +
+Implementation + + +
public(package) fun redeem_fungible_staked_sui(
+    self: &mut SuiSystemStateInnerV2,
+    fungible_staked_sui: FungibleStakedSui,
+    ctx: &TxContext,
+) : Balance<SUI> {
+    self.validators.redeem_fungible_staked_sui(fungible_staked_sui, ctx)
+}
+
+ + +
diff --git a/crates/sui-framework/docs/sui-system/validator.md b/crates/sui-framework/docs/sui-system/validator.md index bda8ac01894c0..54a3c6c86530c 100644 --- a/crates/sui-framework/docs/sui-system/validator.md +++ b/crates/sui-framework/docs/sui-system/validator.md @@ -15,6 +15,8 @@ title: Module `0x3::validator` - [Function `activate`](#0x3_validator_activate) - [Function `adjust_stake_and_gas_price`](#0x3_validator_adjust_stake_and_gas_price) - [Function `request_add_stake`](#0x3_validator_request_add_stake) +- [Function `convert_to_fungible_staked_sui`](#0x3_validator_convert_to_fungible_staked_sui) +- [Function `redeem_fungible_staked_sui`](#0x3_validator_redeem_fungible_staked_sui) - [Function `request_add_stake_at_genesis`](#0x3_validator_request_add_stake_at_genesis) - [Function `request_withdraw_stake`](#0x3_validator_request_withdraw_stake) - [Function `request_set_gas_price`](#0x3_validator_request_set_gas_price) @@ -918,6 +920,62 @@ Request to add stake to the validator's staking pool, processed at the end of th + + + + +## Function `convert_to_fungible_staked_sui` + + + +
public(friend) fun convert_to_fungible_staked_sui(self: &mut validator::Validator, staked_sui: staking_pool::StakedSui, ctx: &mut tx_context::TxContext): staking_pool::FungibleStakedSui
+
+ + + +
+Implementation + + +
public(package) fun convert_to_fungible_staked_sui(
+    self: &mut Validator,
+    staked_sui: StakedSui,
+    ctx: &mut TxContext,
+) : FungibleStakedSui {
+    self.staking_pool.convert_to_fungible_staked_sui(staked_sui, ctx)
+}
+
+ + + +
+ + + +## Function `redeem_fungible_staked_sui` + + + +
public(friend) fun redeem_fungible_staked_sui(self: &mut validator::Validator, fungible_staked_sui: staking_pool::FungibleStakedSui, ctx: &tx_context::TxContext): balance::Balance<sui::SUI>
+
+ + + +
+Implementation + + +
public(package) fun redeem_fungible_staked_sui(
+    self: &mut Validator,
+    fungible_staked_sui: FungibleStakedSui,
+    ctx: &TxContext,
+) : Balance<SUI> {
+    self.staking_pool.redeem_fungible_staked_sui(fungible_staked_sui, ctx)
+}
+
+ + +
diff --git a/crates/sui-framework/docs/sui-system/validator_set.md b/crates/sui-framework/docs/sui-system/validator_set.md index ed59f156c459d..08d75126f194b 100644 --- a/crates/sui-framework/docs/sui-system/validator_set.md +++ b/crates/sui-framework/docs/sui-system/validator_set.md @@ -18,6 +18,8 @@ title: Module `0x3::validator_set` - [Function `request_remove_validator`](#0x3_validator_set_request_remove_validator) - [Function `request_add_stake`](#0x3_validator_set_request_add_stake) - [Function `request_withdraw_stake`](#0x3_validator_set_request_withdraw_stake) +- [Function `convert_to_fungible_staked_sui`](#0x3_validator_set_convert_to_fungible_staked_sui) +- [Function `redeem_fungible_staked_sui`](#0x3_validator_set_redeem_fungible_staked_sui) - [Function `request_set_commission_rate`](#0x3_validator_set_request_set_commission_rate) - [Function `advance_epoch`](#0x3_validator_set_advance_epoch) - [Function `update_and_process_low_stake_departures`](#0x3_validator_set_update_and_process_low_stake_departures) @@ -949,6 +951,85 @@ the stake and any rewards corresponding to it will be immediately processed. + + + + +## Function `convert_to_fungible_staked_sui` + + + +
public(friend) fun convert_to_fungible_staked_sui(self: &mut validator_set::ValidatorSet, staked_sui: staking_pool::StakedSui, ctx: &mut tx_context::TxContext): staking_pool::FungibleStakedSui
+
+ + + +
+Implementation + + +
public(package) fun convert_to_fungible_staked_sui(
+    self: &mut ValidatorSet,
+    staked_sui: StakedSui,
+    ctx: &mut TxContext,
+) : FungibleStakedSui {
+    let staking_pool_id = pool_id(&staked_sui);
+    let validator =
+        if (self.staking_pool_mappings.contains(staking_pool_id)) { // This is an active validator.
+            let validator_address = self.staking_pool_mappings[pool_id(&staked_sui)];
+            get_candidate_or_active_validator_mut(self, validator_address)
+        } else { // This is an inactive pool.
+            assert!(self.inactive_validators.contains(staking_pool_id), ENoPoolFound);
+            let wrapper = &mut self.inactive_validators[staking_pool_id];
+            wrapper.load_validator_maybe_upgrade()
+        };
+
+    validator.convert_to_fungible_staked_sui(staked_sui, ctx)
+}
+
+ + + +
+ + + +## Function `redeem_fungible_staked_sui` + + + +
public(friend) fun redeem_fungible_staked_sui(self: &mut validator_set::ValidatorSet, fungible_staked_sui: staking_pool::FungibleStakedSui, ctx: &tx_context::TxContext): balance::Balance<sui::SUI>
+
+ + + +
+Implementation + + +
public(package) fun redeem_fungible_staked_sui(
+    self: &mut ValidatorSet,
+    fungible_staked_sui: FungibleStakedSui,
+    ctx: &TxContext,
+) : Balance<SUI> {
+    let staking_pool_id = fungible_staked_sui_pool_id(&fungible_staked_sui);
+
+    let validator =
+        if (self.staking_pool_mappings.contains(staking_pool_id)) { // This is an active validator.
+            let validator_address = self.staking_pool_mappings[staking_pool_id];
+            get_candidate_or_active_validator_mut(self, validator_address)
+        } else { // This is an inactive pool.
+            assert!(self.inactive_validators.contains(staking_pool_id), ENoPoolFound);
+            let wrapper = &mut self.inactive_validators[staking_pool_id];
+            wrapper.load_validator_maybe_upgrade()
+        };
+
+    validator.redeem_fungible_staked_sui(fungible_staked_sui, ctx)
+}
+
+ + +
diff --git a/crates/sui-framework/packages_compiled/sui-system b/crates/sui-framework/packages_compiled/sui-system index d576a4d949d827f27ea732656c0f901d9e794308..0b3de9d8c8c5199ae66e1be4bc1e30adac18040e 100644 GIT binary patch delta 16381 zcmbVzd30RYdEdSFt@GZS<;}cVun(5OU>$(O!XXF(7%U`6a3=xo1i^g)7g3~kX+%*H zwUJt$yhxTU$xAHnb{NNw)2K=FM^9_VY1(rdr%hWsC#P=hq&{{|b58!S+uwI*1_UhH zj>AKI@80iz_ulXB-!}j7Z^eK3OYv1zDgGq$Ztah4oiXNc<_T|1+4HwT{if2UeP8)E zit$O~=a%_)^r-&-qJPb;|LPre{xJUCaPav|YxpY}m2KyPe6MgtR1An6Vz-zTbIfuW zGYrN8_zL_f_)`tWb%P_r9Hw%WGp=a{(}G2n>844` zAa0n9r2-aXhitUBPXVqr7-DQ93@{!6C=fX8q97DSs*1p_n8(;~Ji%B~5@1^#*{uY+ zVkyQtVyPly%}L~Es?c&|U|^(fS6))>2zT1R7###&$L$wVptC zJxE%bkjfBf3xPOMA7VT<){TKSj)VYPCr7!$JI7K;=OpY8WjQPIO{4gT)uzXk$WTZa zs^aza(hts$mmyA#~h`dBM7BE!^x5u4gT zpX5}EK7A~Mk2yOL;%1<}$76x%o^dwP((SEj8!Wc1<^64|Ru8nT&iAff*V69gyiTvn z8&x{IHgA2)a0@rgAUCmsCbLa$+KS8ze3&V8lvS|IRFG9ot|*GYpCpjP3g|#A6iF&7 zEit5M`GbC~R$B~k-BdV7HL4m!nIcov6XgUNz*fP3YE9uL=Q?(cLSK^uhv_ZlB@W2| z3vg~)+}3%3fpg-N5(b)5P+8{m0Mm)DqG?O+N<5;NI+Y|6m|)ekF#dp2rVyrPs^H5& zlSY7-i4_$i2gjH-q7PJ>Gc|AsQWOD#+^o!7nbASQsu&F>&yXCWUHsQ&T9IvW4P!z> z zH}s~4`s0@_K5+8N)%mNJ?p4SC<7CSJsovq(C$3$%@bLVpvnLl%_%%j$u;yMpT)uQ^ zasKRys(Y0^u&{XTq(5%FSCu+<>FmXO^>*h+#vHGyzH;)!$&(lEMc|JGrmL&&zr1+% z>OG5}3!Di$S1(;%Sj2!YUtWCJ|BHvbJF4{pFDhmXHzxg4aB$22gq=05FcwFtCa_D{ zD0kF|(oou$d1allMoF6n7h`=U&6lyr87cwTWdi{;$7-|ckpA%CCK7|+dwc!YbFaK~EIq%FA3 zUBkcaR3~*ubCi&VO1i}?-BN7OxQ_p?o%Ui}ov@i}X;U`iLCrSu8n-nZYFvuDY#vh_ z%h4UxM#WrSZB!jC3PLn)h^(!;d7x!ub;M6t4MerP?kKiqS(Xw(kAS-8s*dRxGHF>! zO9|_7TM1KVM$ribao2}&0Q1zh#UjEB1@cM)ND)U?@oXz$v#=7k146|D40eQ=rMNAY zNTN|bV2gp2r3_T}g9YS_fs`$CdWc~LS{&8VEInbX88xG2jd<49Y+Yy=;F`TQFUACT z3t)^ODvvR!#g@isiJUuT1zOc=EnP->XrVOlfLn)oRYGQ7aWmkBMdSpSX>|k)QyPmg zWMOPkH6PG+mXVkt)lxApmXRo0D$Rz9*)VD9L2v7}5yeFc4%gLSNlgzs0aI^k4mhBB zXL6y@xu7Km3nHWt3pOqkOps>U48!7_yLlV;4;l)E!Cb(jjYAU~h=Ucw=6S8iMF8bq z6rZT1dkjw)ks9 zO+^E9(&OlIsCwQIL7E7#ive-BHIL!tTo1b=MAI71QZ}AeJuO9f7td7xg0L zVqL@Vx#46{g!Kpq<06b{3#m3SEh3e)=@o@r$%H%+t)#pe#<4AE5&i@eJ0g)@sG3GD$3JaYALa?j?-e zXe<))Y>vGKQH2$1KKZIupVBrd8@#~T0+19B8_rexHB z7B2UoW^KPG+`e`Q1hj#1F`-%*lV(JPsOBmQ1#Fy89FQs-XRMG9iyB}xtavWO-CFj)eSQH$Xvuq_F^MtJ|?fj&pXyBAusz>W`Ss^SpRMNr|Y1$47Y8Nfh zSi!rVXsV>6o@mCVOu0Bep5)7P1kH@(#pW$Iw?ecKNi!I7y`?kJs0Jh{u+e2$qE~Tb zaii1tTyMbUxaEpOg?mqA{>4q9zHCZ1ik4c@Qb=9mXDJcoB8#GpSRG8Py!9+r#D>Sc zM?2rf6x*sT8r7H@n=d?A?00?bkcbO2S*qoy+r0zV`&ye~j-&i1nd?CsHIdP$>b z+@f6gYgPSsTATJ$?Uri%BJefO{7U^%{pE)Dx%Fr1x484a8}HNx-x?_nhrc&sv6ncH z^Az907ezuOMT^)hE{Ln*ns}bGbL=+9O-zG=nu3LM6vi!sDJrQbDpDHM6kVz*red&w zE|_f#<^&BE#MKgn>Jl{YH62_5&{&2@W66`oQmYoBfViZ$#7J*B2w|7iklu2D^p-;g zaPvW=j)agpPT(*RA0yBZ2AGWita78!gIow17hT8~{cfDGG=VM`YRnvgLCS6=u#14t z3GQ=~&}jC$NxWkOUgreITqr2x1SSY$Jlwd9;#476#e(4>0Zk<5*%(qr6+ka;1dLj1fG-m>k>Xl zh#NBbFeRUs$ww&ptV}*e$!B;A;G+bdlGuj`ds8MKr(`3|_6t0wq5l_o2AK{Q`*v*> zOS_oqX5zk$8SJ*I>5YJ`Giw2Rr@^4ITEa~l^eN@!TGFtx-5pHJ_w+LT72aRNnbEXX z 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 I&Fg{x56bl_y8r+H delta 14347 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 Lv#{;Mr(*vPbvCT) diff --git a/crates/sui-framework/published_api.txt b/crates/sui-framework/published_api.txt index 64879af4a2a3f..8dba844c75cc1 100644 --- a/crates/sui-framework/published_api.txt +++ b/crates/sui-framework/published_api.txt @@ -25,6 +25,15 @@ PoolTokenExchangeRate StakedSui public struct 0x3::staking_pool +FungibleStakedSui + public struct + 0x3::staking_pool +FungibleStakedSuiData + public struct + 0x3::staking_pool +FungibleStakedSuiDataKey + public struct + 0x3::staking_pool new public(package) fun 0x3::staking_pool @@ -34,6 +43,15 @@ request_add_stake request_withdraw_stake public(package) fun 0x3::staking_pool +redeem_fungible_staked_sui + public(package) fun + 0x3::staking_pool +calculate_fungible_staked_sui_withdraw_amount + fun + 0x3::staking_pool +convert_to_fungible_staked_sui + public(package) fun + 0x3::staking_pool withdraw_from_principal public(package) fun 0x3::staking_pool @@ -67,6 +85,9 @@ sui_balance pool_id public fun 0x3::staking_pool +fungible_staked_sui_pool_id + public fun + 0x3::staking_pool staked_sui_amount public fun 0x3::staking_pool @@ -79,6 +100,15 @@ is_preactive is_inactive public fun 0x3::staking_pool +fungible_staked_sui_value + public fun + 0x3::staking_pool +split_fungible_staked_sui + public fun + 0x3::staking_pool +join_fungible_staked_sui + public fun + 0x3::staking_pool split public fun 0x3::staking_pool @@ -154,6 +184,12 @@ adjust_stake_and_gas_price request_add_stake public(package) fun 0x3::validator +convert_to_fungible_staked_sui + public(package) fun + 0x3::validator +redeem_fungible_staked_sui + public(package) fun + 0x3::validator request_add_stake_at_genesis public(package) fun 0x3::validator @@ -457,6 +493,12 @@ request_add_stake request_withdraw_stake public(package) fun 0x3::validator_set +convert_to_fungible_staked_sui + public(package) fun + 0x3::validator_set +redeem_fungible_staked_sui + public(package) fun + 0x3::validator_set request_set_commission_rate public(package) fun 0x3::validator_set @@ -700,6 +742,12 @@ request_add_stake_mul_coin request_withdraw_stake public(package) fun 0x3::sui_system_state_inner +convert_to_fungible_staked_sui + public(package) fun + 0x3::sui_system_state_inner +redeem_fungible_staked_sui + public(package) fun + 0x3::sui_system_state_inner report_validator public(package) fun 0x3::sui_system_state_inner @@ -862,6 +910,12 @@ request_add_stake_mul_coin request_withdraw_stake public entry fun 0x3::sui_system +convert_to_fungible_staked_sui + public fun + 0x3::sui_system +redeem_fungible_staked_sui + public fun + 0x3::sui_system request_withdraw_stake_non_entry public fun 0x3::sui_system From af1f9198d364d45876dd85ffb3363bee6a62a95b Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Mon, 5 Aug 2024 13:17:46 -0400 Subject: [PATCH 04/10] restore Move.lock --- crates/sui-framework/packages/sui-system/Move.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/sui-framework/packages/sui-system/Move.lock b/crates/sui-framework/packages/sui-system/Move.lock index a5b11a5ebb2b1..af204d00c7990 100644 --- a/crates/sui-framework/packages/sui-system/Move.lock +++ b/crates/sui-framework/packages/sui-system/Move.lock @@ -23,6 +23,6 @@ dependencies = [ ] [move.toolchain-version] -compiler-version = "1.29.0" -edition = "2024.beta" +compiler-version = "1.22.0" +edition = "legacy" flavor = "sui" From 14050a630b24c870bd888ca5e7d261aac740b9d0 Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Tue, 3 Sep 2024 15:57:59 -0400 Subject: [PATCH 05/10] underflow fix --- .../sui-system/sources/staking_pool.move | 6 +++ .../sui-system/tests/staking_pool.move | 49 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/crates/sui-framework/packages/sui-system/sources/staking_pool.move b/crates/sui-framework/packages/sui-system/sources/staking_pool.move index 52cb5596bbe8e..901e6776c214e 100644 --- a/crates/sui-framework/packages/sui-system/sources/staking_pool.move +++ b/crates/sui-framework/packages/sui-system/sources/staking_pool.move @@ -232,6 +232,12 @@ module sui_system::staking_pool { // 1. if the entire FungibleStakedSuiData supply is redeemed, how much sui should we receive? let total_sui_amount = get_sui_amount(&latest_exchange_rate, fungible_staked_sui_data_total_supply); + // min with total_sui_amount to prevent underflow + let fungible_staked_sui_data_principal_amount = std::u64::min( + fungible_staked_sui_data_principal_amount, + total_sui_amount + ); + // 2. how much do we need to withdraw from the rewards pool? let total_rewards = total_sui_amount - fungible_staked_sui_data_principal_amount; diff --git a/crates/sui-framework/packages/sui-system/tests/staking_pool.move b/crates/sui-framework/packages/sui-system/tests/staking_pool.move index d5ed2cdef5599..3ebfbac592681 100644 --- a/crates/sui-framework/packages/sui-system/tests/staking_pool.move +++ b/crates/sui-framework/packages/sui-system/tests/staking_pool.move @@ -281,6 +281,55 @@ module sui_system::staking_pool_tests { scenario.end(); } + #[test] + fun test_redeem_fungible_staked_sui_regression_rounding() { + let mut scenario = test_scenario::begin(@0x0); + let mut staking_pool = staking_pool::new(scenario.ctx()); + staking_pool.activate_staking_pool(0); + + // setup + + let sui = balance::create_for_testing(1_000_000_000); + let staked_sui_1 = staking_pool.request_add_stake(sui, scenario.ctx().epoch() + 1, scenario.ctx()); + + assert!(distribute_rewards_and_advance_epoch(&mut staking_pool, &mut scenario, 0) == 1, 0); + + let latest_exchange_rate = staking_pool.pool_token_exchange_rate_at_epoch(1); + assert!(latest_exchange_rate.sui_amount() == 1_000_000_000, 0); + assert!(latest_exchange_rate.pool_token_amount() == 1_000_000_000, 0); + + let sui = balance::create_for_testing(1_000_000_001); + let staked_sui_2 = staking_pool.request_add_stake(sui, scenario.ctx().epoch() + 1, scenario.ctx()); + + assert!(distribute_rewards_and_advance_epoch(&mut staking_pool, &mut scenario, 1_000_000_000) == 2, 0); + + let latest_exchange_rate = staking_pool.pool_token_exchange_rate_at_epoch(2); + assert!(latest_exchange_rate.sui_amount() == 3_000_000_001, 0); + assert!(latest_exchange_rate.pool_token_amount() == 1_500_000_000, 0); + + let fungible_staked_sui = staking_pool.convert_to_fungible_staked_sui(staked_sui_2, scenario.ctx()); + assert!(fungible_staked_sui.value() == 500_000_000, 0); // rounding! + assert!(fungible_staked_sui.pool_id() == object::id(&staking_pool), 0); + + let fungible_staked_sui_data = staking_pool.fungible_staked_sui_data(); + assert!(fungible_staked_sui_data.total_supply() == 500_000_000, 0); + assert!(fungible_staked_sui_data.principal_value() == 1_000_000_001, 0); + + // this line used to error + let sui = staking_pool.redeem_fungible_staked_sui(fungible_staked_sui, scenario.ctx()); + assert!(sui.value() == 1_000_000_000, 0); + + let fungible_staked_sui_data = staking_pool.fungible_staked_sui_data(); + assert!(fungible_staked_sui_data.total_supply() == 0, 0); + assert!(fungible_staked_sui_data.principal_value() == 1, 0); + + sui::test_utils::destroy(staking_pool); + sui::test_utils::destroy(staked_sui_1); + sui::test_utils::destroy(sui); + + scenario.end(); + } + #[test_only] fun distribute_rewards_and_advance_epoch( staking_pool: &mut StakingPool, From 680cbce61aadbcc3a9b1269d5980b659418b806b Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Tue, 3 Sep 2024 16:00:53 -0400 Subject: [PATCH 06/10] other audit fixes --- .../sui-framework/packages/sui-system/sources/staking_pool.move | 2 +- .../packages/sui-system/sources/validator_set.move | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/sui-framework/packages/sui-system/sources/staking_pool.move b/crates/sui-framework/packages/sui-system/sources/staking_pool.move index 901e6776c214e..7dfd16fdc440f 100644 --- a/crates/sui-framework/packages/sui-system/sources/staking_pool.move +++ b/crates/sui-framework/packages/sui-system/sources/staking_pool.move @@ -477,7 +477,7 @@ module sui_system::staking_pool { split_amount: u64, ctx: &mut TxContext ): FungibleStakedSui { - assert!(split_amount <= fungible_staked_sui.value, EInsufficientPoolTokenBalance); + assert!(split_amount < fungible_staked_sui.value, EInsufficientPoolTokenBalance); fungible_staked_sui.value = fungible_staked_sui.value - split_amount; diff --git a/crates/sui-framework/packages/sui-system/sources/validator_set.move b/crates/sui-framework/packages/sui-system/sources/validator_set.move index 3f631642fa577..a5deb4e148a91 100644 --- a/crates/sui-framework/packages/sui-system/sources/validator_set.move +++ b/crates/sui-framework/packages/sui-system/sources/validator_set.move @@ -317,7 +317,7 @@ module sui_system::validator_set { let staking_pool_id = pool_id(&staked_sui); let validator = if (self.staking_pool_mappings.contains(staking_pool_id)) { // This is an active validator. - let validator_address = self.staking_pool_mappings[pool_id(&staked_sui)]; + let validator_address = self.staking_pool_mappings[staking_pool_id]; get_candidate_or_active_validator_mut(self, validator_address) } else { // This is an inactive pool. assert!(self.inactive_validators.contains(staking_pool_id), ENoPoolFound); From 50fb2b779c55d944db7eaf3cb3731caec39245e1 Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Thu, 12 Sep 2024 00:08:37 -0400 Subject: [PATCH 07/10] remove FungibleStakedSui.split constraints --- .../sui-system/sources/staking_pool.move | 5 +-- .../sui-system/tests/staking_pool.move | 34 ------------------- 2 files changed, 1 insertion(+), 38 deletions(-) diff --git a/crates/sui-framework/packages/sui-system/sources/staking_pool.move b/crates/sui-framework/packages/sui-system/sources/staking_pool.move index 7dfd16fdc440f..ba10693c3dccf 100644 --- a/crates/sui-framework/packages/sui-system/sources/staking_pool.move +++ b/crates/sui-framework/packages/sui-system/sources/staking_pool.move @@ -477,13 +477,10 @@ module sui_system::staking_pool { split_amount: u64, ctx: &mut TxContext ): FungibleStakedSui { - assert!(split_amount < fungible_staked_sui.value, EInsufficientPoolTokenBalance); + assert!(split_amount <= fungible_staked_sui.value, EInsufficientPoolTokenBalance); fungible_staked_sui.value = fungible_staked_sui.value - split_amount; - assert!(fungible_staked_sui.value >= MIN_STAKING_THRESHOLD, EStakedSuiBelowThreshold); - assert!(split_amount >= MIN_STAKING_THRESHOLD, EStakedSuiBelowThreshold); - FungibleStakedSui { id: object::new(ctx), pool_id: fungible_staked_sui.pool_id, diff --git a/crates/sui-framework/packages/sui-system/tests/staking_pool.move b/crates/sui-framework/packages/sui-system/tests/staking_pool.move index 3ebfbac592681..2a38e5f249aa8 100644 --- a/crates/sui-framework/packages/sui-system/tests/staking_pool.move +++ b/crates/sui-framework/packages/sui-system/tests/staking_pool.move @@ -63,40 +63,6 @@ module sui_system::staking_pool_tests { scenario.end(); } - #[test] - #[expected_failure(abort_code = 18, location = sui_system::staking_pool)] - fun test_split_fungible_staked_sui_fail_too_little_1() { - let mut scenario = test_scenario::begin(@0x0); - let staking_pool = staking_pool::new(scenario.ctx()); - - let mut fungible_staked_sui_1 = staking_pool.create_fungible_staked_sui_for_testing(100_000_000_000, scenario.ctx()); - - let fungible_staked_sui_2 = fungible_staked_sui_1.split(100_000_000_000 - 1, scenario.ctx()); - - sui::test_utils::destroy(staking_pool); - sui::test_utils::destroy(fungible_staked_sui_1); - sui::test_utils::destroy(fungible_staked_sui_2); - - scenario.end(); - } - - #[test] - #[expected_failure(abort_code = 18, location = sui_system::staking_pool)] - fun test_split_fungible_staked_sui_fail_too_little_2() { - let mut scenario = test_scenario::begin(@0x0); - let staking_pool = staking_pool::new(scenario.ctx()); - - let mut fungible_staked_sui_1 = staking_pool.create_fungible_staked_sui_for_testing(100_000_000_000, scenario.ctx()); - - let fungible_staked_sui_2 = fungible_staked_sui_1.split(1, scenario.ctx()); - - sui::test_utils::destroy(staking_pool); - sui::test_utils::destroy(fungible_staked_sui_1); - sui::test_utils::destroy(fungible_staked_sui_2); - - scenario.end(); - } - #[test] #[expected_failure(abort_code = 0, location = sui_system::staking_pool)] fun test_split_fungible_staked_sui_fail_too_much() { From 65f16b2502c36617121653259c62ea2d468d7ea0 Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Thu, 12 Sep 2024 01:32:32 -0400 Subject: [PATCH 08/10] add events for FungibleStakedSui creation and redemption --- .../sui-system/sources/validator.move | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/crates/sui-framework/packages/sui-system/sources/validator.move b/crates/sui-framework/packages/sui-system/sources/validator.move index b3f8e3d6f3a09..da6157014541e 100644 --- a/crates/sui-framework/packages/sui-system/sources/validator.move +++ b/crates/sui-framework/packages/sui-system/sources/validator.move @@ -160,6 +160,21 @@ module sui_system::validator { reward_amount: u64, } + /// Event emitted when a staked SUI is converted to a fungible staked SUI. + public struct ConvertingToFungibleStakedSuiEvent has copy, drop { + pool_id: ID, + stake_activation_epoch: u64, + staked_sui_principal_amount: u64, + fungible_staked_sui_amount: u64, + } + + /// Event emitted when a fungible staked SUI is redeemed. + public struct RedeemingFungibleStakedSuiEvent has copy, drop { + pool_id: ID, + fungible_staked_sui_amount: u64, + sui_amount: u64, + } + public(package) fun new_metadata( sui_address: address, protocol_pubkey_bytes: vector, @@ -311,7 +326,21 @@ module sui_system::validator { staked_sui: StakedSui, ctx: &mut TxContext, ) : FungibleStakedSui { - self.staking_pool.convert_to_fungible_staked_sui(staked_sui, ctx) + let stake_activation_epoch = staked_sui.stake_activation_epoch(); + let staked_sui_principal_amount = staked_sui.staked_sui_amount(); + + let fungible_staked_sui = self.staking_pool.convert_to_fungible_staked_sui(staked_sui, ctx); + + event::emit( + ConvertingToFungibleStakedSuiEvent { + pool_id: self.staking_pool_id(), + stake_activation_epoch, + staked_sui_principal_amount, + fungible_staked_sui_amount: fungible_staked_sui.value(), + } + ); + + fungible_staked_sui } public(package) fun redeem_fungible_staked_sui( @@ -319,7 +348,19 @@ module sui_system::validator { fungible_staked_sui: FungibleStakedSui, ctx: &TxContext, ) : Balance { - self.staking_pool.redeem_fungible_staked_sui(fungible_staked_sui, ctx) + let fungible_staked_sui_amount = fungible_staked_sui.value(); + + let sui = self.staking_pool.redeem_fungible_staked_sui(fungible_staked_sui, ctx); + + event::emit( + RedeemingFungibleStakedSuiEvent { + pool_id: self.staking_pool_id(), + fungible_staked_sui_amount, + sui_amount: sui.value(), + } + ); + + sui } /// Request to add stake to the validator's staking pool at genesis From dbdd521333f06793ed16d6d0ea58d9978be26a7c Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Fri, 27 Sep 2024 16:05:47 +0800 Subject: [PATCH 09/10] update docs --- .../docs/sui-system/staking_pool.md | 9 +- .../docs/sui-system/validator.md | 118 +++++++++++++++++- .../docs/sui-system/validator_set.md | 2 +- .../packages_compiled/sui-system | Bin 43984 -> 44241 bytes crates/sui-framework/published_api.txt | 6 + 5 files changed, 129 insertions(+), 6 deletions(-) diff --git a/crates/sui-framework/docs/sui-system/staking_pool.md b/crates/sui-framework/docs/sui-system/staking_pool.md index 747c82c18161a..f51eff50b2fa2 100644 --- a/crates/sui-framework/docs/sui-system/staking_pool.md +++ b/crates/sui-framework/docs/sui-system/staking_pool.md @@ -771,6 +771,12 @@ returns (principal_withdraw_amount, rewards_withdraw_amount) // 1. if the entire FungibleStakedSuiData supply is redeemed, how much sui should we receive? let total_sui_amount = get_sui_amount(&latest_exchange_rate, fungible_staked_sui_data_total_supply); + // min with total_sui_amount to prevent underflow + let fungible_staked_sui_data_principal_amount = std::u64::min( + fungible_staked_sui_data_principal_amount, + total_sui_amount + ); + // 2. how much do we need to withdraw from the rewards pool? let total_rewards = total_sui_amount - fungible_staked_sui_data_principal_amount; @@ -1377,9 +1383,6 @@ Returns true if the input staking pool is inactive. fungible_staked_sui.value = fungible_staked_sui.value - split_amount; - assert!(fungible_staked_sui.value >= MIN_STAKING_THRESHOLD, EStakedSuiBelowThreshold); - assert!(split_amount >= MIN_STAKING_THRESHOLD, EStakedSuiBelowThreshold); - FungibleStakedSui { id: object::new(ctx), pool_id: fungible_staked_sui.pool_id, diff --git a/crates/sui-framework/docs/sui-system/validator.md b/crates/sui-framework/docs/sui-system/validator.md index 54a3c6c86530c..db50fa3400a9d 100644 --- a/crates/sui-framework/docs/sui-system/validator.md +++ b/crates/sui-framework/docs/sui-system/validator.md @@ -8,6 +8,8 @@ title: Module `0x3::validator` - [Struct `Validator`](#0x3_validator_Validator) - [Struct `StakingRequestEvent`](#0x3_validator_StakingRequestEvent) - [Struct `UnstakingRequestEvent`](#0x3_validator_UnstakingRequestEvent) +- [Struct `ConvertingToFungibleStakedSuiEvent`](#0x3_validator_ConvertingToFungibleStakedSuiEvent) +- [Struct `RedeemingFungibleStakedSuiEvent`](#0x3_validator_RedeemingFungibleStakedSuiEvent) - [Constants](#@Constants_0) - [Function `new_metadata`](#0x3_validator_new_metadata) - [Function `new`](#0x3_validator_new) @@ -461,6 +463,92 @@ Event emitted when a new unstake request is received. + + + + +## Struct `ConvertingToFungibleStakedSuiEvent` + +Event emitted when a staked SUI is converted to a fungible staked SUI. + + +
struct ConvertingToFungibleStakedSuiEvent has copy, drop
+
+ + + +
+Fields + + +
+
+pool_id: object::ID +
+
+ +
+
+stake_activation_epoch: u64 +
+
+ +
+
+staked_sui_principal_amount: u64 +
+
+ +
+
+fungible_staked_sui_amount: u64 +
+
+ +
+
+ + +
+ + + +## Struct `RedeemingFungibleStakedSuiEvent` + +Event emitted when a fungible staked SUI is redeemed. + + +
struct RedeemingFungibleStakedSuiEvent has copy, drop
+
+ + + +
+Fields + + +
+
+pool_id: object::ID +
+
+ +
+
+fungible_staked_sui_amount: u64 +
+
+ +
+
+sui_amount: u64 +
+
+ +
+
+ +
@@ -942,7 +1030,21 @@ Request to add stake to the validator's staking pool, processed at the end of th staked_sui: StakedSui, ctx: &mut TxContext, ) : FungibleStakedSui { - self.staking_pool.convert_to_fungible_staked_sui(staked_sui, ctx) + let stake_activation_epoch = staked_sui.stake_activation_epoch(); + let staked_sui_principal_amount = staked_sui.staked_sui_amount(); + + let fungible_staked_sui = self.staking_pool.convert_to_fungible_staked_sui(staked_sui, ctx); + + event::emit( + ConvertingToFungibleStakedSuiEvent { + pool_id: self.staking_pool_id(), + stake_activation_epoch, + staked_sui_principal_amount, + fungible_staked_sui_amount: fungible_staked_sui.value(), + } + ); + + fungible_staked_sui } @@ -970,7 +1072,19 @@ Request to add stake to the validator's staking pool, processed at the end of th fungible_staked_sui: FungibleStakedSui, ctx: &TxContext, ) : Balance<SUI> { - self.staking_pool.redeem_fungible_staked_sui(fungible_staked_sui, ctx) + let fungible_staked_sui_amount = fungible_staked_sui.value(); + + let sui = self.staking_pool.redeem_fungible_staked_sui(fungible_staked_sui, ctx); + + event::emit( + RedeemingFungibleStakedSuiEvent { + pool_id: self.staking_pool_id(), + fungible_staked_sui_amount, + sui_amount: sui.value(), + } + ); + + sui } diff --git a/crates/sui-framework/docs/sui-system/validator_set.md b/crates/sui-framework/docs/sui-system/validator_set.md index 08d75126f194b..5e38d48f25c0c 100644 --- a/crates/sui-framework/docs/sui-system/validator_set.md +++ b/crates/sui-framework/docs/sui-system/validator_set.md @@ -976,7 +976,7 @@ the stake and any rewards corresponding to it will be immediately processed. let staking_pool_id = pool_id(&staked_sui); let validator = if (self.staking_pool_mappings.contains(staking_pool_id)) { // This is an active validator. - let validator_address = self.staking_pool_mappings[pool_id(&staked_sui)]; + let validator_address = self.staking_pool_mappings[staking_pool_id]; get_candidate_or_active_validator_mut(self, validator_address) } else { // This is an inactive pool. assert!(self.inactive_validators.contains(staking_pool_id), ENoPoolFound); diff --git a/crates/sui-framework/packages_compiled/sui-system b/crates/sui-framework/packages_compiled/sui-system index 0b3de9d8c8c5199ae66e1be4bc1e30adac18040e..2f5cc8064c3d9535e1ec7fc97f87d22e2eacf020 100644 GIT binary patch delta 2890 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 diff --git a/crates/sui-framework/published_api.txt b/crates/sui-framework/published_api.txt index 8dba844c75cc1..e6d0acfbb736b 100644 --- a/crates/sui-framework/published_api.txt +++ b/crates/sui-framework/published_api.txt @@ -166,6 +166,12 @@ StakingRequestEvent UnstakingRequestEvent public struct 0x3::validator +ConvertingToFungibleStakedSuiEvent + public struct + 0x3::validator +RedeemingFungibleStakedSuiEvent + public struct + 0x3::validator new_metadata public(package) fun 0x3::validator From de5c4d5af8cb43bd694f2d08566f3bb24376565f Mon Sep 17 00:00:00 2001 From: Emma Zhong Date: Fri, 27 Sep 2024 12:28:43 -0700 Subject: [PATCH 10/10] rebase and update snapshot --- ..._populated_genesis_snapshot_matches-2.snap | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap b/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap index ca9bc6e3d6d27..dbba45244911a 100644 --- a/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap +++ b/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap @@ -240,13 +240,13 @@ validators: next_epoch_worker_address: ~ extra_fields: id: - id: "0xc7e49487dc27703828d38516b64e1fe8841b678422efb3258c48cd1ab0f5fbc8" + id: "0x7f26b902d13d30b22172a17d566502252f3d5e1b2fbcb62830b0e57f628bace1" size: 0 voting_power: 10000 - operation_cap_id: "0x037ae08f2fba1b711149b5f8502b7bc04d30caf3142f0ef0e0cffeb87f05a4ae" + operation_cap_id: "0x54279c3f967c7fca5839484ca0f47e7c03b3c944243ec1441026f9d8c3d174a9" gas_price: 1000 staking_pool: - id: "0x35b6eb200b1c83dcf097c7a1fdd55cf2ebe7e9f91348eeb61392754bf6b75505" + id: "0xac1b9af5c5da96a0cd0a8efd2e1112b494f7265fb1d656d568f7decb332781b0" activation_epoch: 0 deactivation_epoch: ~ sui_balance: 20000000000000000 @@ -254,14 +254,14 @@ validators: value: 0 pool_token_balance: 20000000000000000 exchange_rates: - id: "0xdf7cb13037359145b99e75cd2c241c8ec54e27f3e2c3e5130d6f5330efeb9e89" + id: "0x3e2414a6732e6605ae258347e36f6c6c7d268af6ca3d8cc070d744a15b06ec87" size: 1 pending_stake: 0 pending_total_sui_withdraw: 0 pending_pool_token_withdraw: 0 extra_fields: id: - id: "0x44d0fc1421e9f12c952de950ef4022de7f9dfa9a6c382b343ce3415465e85e0d" + id: "0x6b4794e72ae76f1c4680f29c8123b962b07d05128b6ea5d48f726fd09ded575e" size: 0 commission_rate: 200 next_epoch_stake: 20000000000000000 @@ -269,27 +269,27 @@ validators: next_epoch_commission_rate: 200 extra_fields: id: - id: "0xed5ac6e9315b7bfd93b9ae34abfef6f4b4fa75cbba83b95e216466a0be9c16a2" + id: "0xa48763deda8ed12d687c56744316bc2861e24c1ef3bccb3ee5ffb5ebc3610749" size: 0 pending_active_validators: contents: - id: "0xb41cf445c30bedd43adda7389b9f26ff5761097a74aff8c6abc9ab8390bbc41d" + id: "0xe86894781a668264600f6c03a0451f34e5a3da852f223d404a45d253bd9aedd5" size: 0 pending_removals: [] staking_pool_mappings: - id: "0x6b5813504fb66e6aacd2f9ce7c84c656f770c7cbdb7b901a5dcbd620a02544aa" + id: "0x7e541610d3b1718495c89ff5f652ea474bb44077eb59fbcac8d329dc426fb9bd" size: 1 inactive_validators: - id: "0x6ad4dc51b60eb86ca8ee742c24d63752bed3dc4340ee11f4108f1d768445f67f" + id: "0xc721d0b70ff119f66360acff8acf1d67b686d74406527b2cfb4053b0c94ae9e4" size: 0 validator_candidates: - id: "0x2dceedb281425ca7eec8b2d0e65863a806cd0384a2a8eda97a88785c77d5d204" + id: "0x3fbdf26fe072eff58a488d6d87d58b8b0001e65bb7f8b795a03bbc00125ac8bf" size: 0 at_risk_validators: contents: [] extra_fields: id: - id: "0x064d5f61130f3938c57c13d9fa1e887a761a02fa375510f4f41fcd1e2d52997a" + id: "0x819e2eb9e011d08a696c9001882d0e7632da71b31415998d408bea36b10cfc5c" size: 0 storage_fund: total_object_storage_rebates: @@ -306,7 +306,7 @@ parameters: validator_low_stake_grace_period: 7 extra_fields: id: - id: "0x5cc2ff41ed3befe2a5e596841685eac8e584b4f14c196e41b7a63ae77ddd3c3a" + id: "0x6f1b019b4062b3000dea8d95af3d772ca1629d507c229a420afd9893027ae3c3" size: 0 reference_gas_price: 1000 validator_report_records: @@ -320,7 +320,7 @@ stake_subsidy: stake_subsidy_decrease_rate: 1000 extra_fields: id: - id: "0xe8c2342b9e93ce6a55e58d422c8e3c228105287665b6c1c963535d8c6f1c7418" + id: "0x27e165381cd7d6926b35da3ce0cb9fa403c859722842a64829f3f0a7d74f2114" size: 0 safe_mode: false safe_mode_storage_rewards: @@ -332,6 +332,5 @@ safe_mode_non_refundable_storage_fee: 0 epoch_start_timestamp_ms: 10 extra_fields: id: - id: "0x0055b6e98f166455d70a76c51bfa0ca92aae5fdf7636f116d6f3cb531906cbc8" + id: "0xfa9ad544627e85e5427fd2a4047797ad8f7d3e5455ccf37c89b5b85348e48780" size: 0 -