diff --git a/crates/sui-framework/docs/sui-system/staking_pool.md b/crates/sui-framework/docs/sui-system/staking_pool.md index 782ad81ed3cc1..f51eff50b2fa2 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,193 @@ 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);
+
+    // 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;
+
+    // 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 +1217,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 +1335,93 @@ 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;
+
+    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..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) @@ -15,6 +17,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) @@ -459,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 +
+
+ +
+
+ +
@@ -918,6 +1008,88 @@ 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 {
+    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
+}
+
+ + + +
+ + + +## 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> {
+    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 ed59f156c459d..5e38d48f25c0c 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[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.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/sui-system/sources/staking_pool.move b/crates/sui-framework/packages/sui-system/sources/staking_pool.move index 0295dbfa703a1..ba10693c3dccf 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,133 @@ 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); + + // 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; + + // 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 +446,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 +468,36 @@ 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; + + 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 +659,104 @@ 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 {}) + } + + #[test_only] + 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 + } + + #[test_only] + 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..da6157014541e 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; @@ -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, @@ -306,6 +321,48 @@ module sui_system::validator { staked_sui } + public(package) fun convert_to_fungible_staked_sui( + self: &mut Validator, + staked_sui: StakedSui, + ctx: &mut TxContext, + ) : FungibleStakedSui { + 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( + self: &mut Validator, + fungible_staked_sui: FungibleStakedSui, + ctx: &TxContext, + ) : Balance { + 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 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..a5deb4e148a91 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[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.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..2a38e5f249aa8 --- /dev/null +++ b/crates/sui-framework/packages/sui-system/tests/staking_pool.move @@ -0,0 +1,317 @@ +// 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 = 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] + 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, + 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(); + } + } diff --git a/crates/sui-framework/packages_compiled/sui-system b/crates/sui-framework/packages_compiled/sui-system index d576a4d949d82..2f5cc8064c3d9 100644 Binary files a/crates/sui-framework/packages_compiled/sui-system and b/crates/sui-framework/packages_compiled/sui-system differ diff --git a/crates/sui-framework/published_api.txt b/crates/sui-framework/published_api.txt index 64879af4a2a3f..e6d0acfbb736b 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 @@ -136,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 @@ -154,6 +190,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 +499,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 +748,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 +916,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 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 -