From cf3d174a697aaed4ad8e069baf6dbed87f8ed265 Mon Sep 17 00:00:00 2001 From: Simon Noetzlin Date: Tue, 28 Nov 2023 17:56:55 +0100 Subject: [PATCH] minimum refactor to build --- .../integration/staking/keeper/common_test.go | 6 + .../staking/keeper/genesis_test.go | 44 + .../testutil/expected_keepers_mocks.go | 114 +- x/slashing/keeper/hooks.go | 4 + x/staking/abci.go | 1 + x/staking/client/cli/query.go | 331 +++++ x/staking/client/cli/tx.go | 286 ++++ x/staking/keeper/liquid_stake.go | 430 ++++++ x/staking/keeper/liquid_stake_test.go | 1207 +++++++++++++++++ x/staking/keeper/msg_server.go | 596 +++++++- x/staking/keeper/params.go | 17 + x/staking/simulation/genesis.go | 30 +- x/staking/testutil/expected_keepers_mocks.go | 56 + x/staking/types/events.go | 23 +- x/staking/types/expected_keepers.go | 4 + x/staking/types/hooks.go | 9 + x/staking/types/msg.go | 327 ++++- x/staking/types/params.go | 95 +- x/staking/types/params_legacy.go | 18 +- x/staking/types/validator.go | 7 +- 20 files changed, 3483 insertions(+), 122 deletions(-) create mode 100644 x/staking/keeper/liquid_stake.go create mode 100644 x/staking/keeper/liquid_stake_test.go diff --git a/tests/integration/staking/keeper/common_test.go b/tests/integration/staking/keeper/common_test.go index 21acfe1599a9..f6169b9d44c1 100644 --- a/tests/integration/staking/keeper/common_test.go +++ b/tests/integration/staking/keeper/common_test.go @@ -88,3 +88,9 @@ func createValidators(t *testing.T, ctx sdk.Context, app *simapp.SimApp, powers return addrs, valAddrs, vals } + +func delegateCoinsFromAccount(ctx sdk.Context, app *simapp.SimApp, addr sdk.AccAddress, amount sdk.Int, val types.Validator) error { + _, err := app.StakingKeeper.Delegate(ctx, addr, amount, types.Unbonded, val, true) + + return err +} diff --git a/tests/integration/staking/keeper/genesis_test.go b/tests/integration/staking/keeper/genesis_test.go index 608e84f9751f..4e3409f4e9fa 100644 --- a/tests/integration/staking/keeper/genesis_test.go +++ b/tests/integration/staking/keeper/genesis_test.go @@ -3,6 +3,7 @@ package keeper_test import ( "fmt" "testing" + "time" "cosmossdk.io/math" abci "github.com/cometbft/cometbft/abci/types" @@ -218,3 +219,46 @@ func TestInitGenesisLargeValidatorSet(t *testing.T) { vals = vals[:100] require.Equal(t, abcivals, vals) } + +func TestInitExportLiquidStakingGenesis(t *testing.T) { + app, ctx, addrs := bootstrapGenesisTest(t, 2) + address1, address2 := addrs[0], addrs[1] + + // Mock out a genesis state + inGenesisState := types.GenesisState{ + Params: types.DefaultParams(), + TokenizeShareRecords: []types.TokenizeShareRecord{ + {Id: 1, Owner: address1.String(), ModuleAccount: "module1", Validator: "val1"}, + {Id: 2, Owner: address2.String(), ModuleAccount: "module2", Validator: "val2"}, + }, + LastTokenizeShareRecordId: 2, + TotalLiquidStakedTokens: sdk.NewInt(1_000_000), + TokenizeShareLocks: []types.TokenizeShareLock{ + { + Address: address1.String(), + Status: types.TOKENIZE_SHARE_LOCK_STATUS_LOCKED.String(), + }, + { + Address: address2.String(), + Status: types.TOKENIZE_SHARE_LOCK_STATUS_LOCK_EXPIRING.String(), + CompletionTime: time.Date(2023, 1, 1, 1, 0, 0, 0, time.UTC), + }, + }, + } + + // Call init and then export genesis - confirming the same state is returned + staking.InitGenesis(ctx, app.StakingKeeper, app.AccountKeeper, app.BankKeeper, &inGenesisState) + outGenesisState := *staking.ExportGenesis(ctx, app.StakingKeeper) + + require.ElementsMatch(t, inGenesisState.TokenizeShareRecords, outGenesisState.TokenizeShareRecords, + "tokenize share records") + + require.Equal(t, inGenesisState.LastTokenizeShareRecordId, outGenesisState.LastTokenizeShareRecordId, + "last tokenize share record ID") + + require.Equal(t, inGenesisState.TotalLiquidStakedTokens.Int64(), outGenesisState.TotalLiquidStakedTokens.Int64(), + "total liquid staked") + + require.ElementsMatch(t, inGenesisState.TokenizeShareLocks, outGenesisState.TokenizeShareLocks, + "tokenize share locks") +} diff --git a/x/distribution/testutil/expected_keepers_mocks.go b/x/distribution/testutil/expected_keepers_mocks.go index c51a8ec771d0..b3de49ec23c6 100644 --- a/x/distribution/testutil/expected_keepers_mocks.go +++ b/x/distribution/testutil/expected_keepers_mocks.go @@ -141,6 +141,20 @@ func (mr *MockBankKeeperMockRecorder) GetAllBalances(ctx, addr interface{}) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllBalances", reflect.TypeOf((*MockBankKeeper)(nil).GetAllBalances), ctx, addr) } +// SendCoins mocks base method. +func (m *MockBankKeeper) SendCoins(ctx types.Context, fromAddr, toAddr types.AccAddress, amt types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendCoins", ctx, fromAddr, toAddr, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendCoins indicates an expected call of SendCoins. +func (mr *MockBankKeeperMockRecorder) SendCoins(ctx, fromAddr, toAddr, amt interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoins", reflect.TypeOf((*MockBankKeeper)(nil).SendCoins), ctx, fromAddr, toAddr, amt) +} + // SendCoinsFromAccountToModule mocks base method. func (m *MockBankKeeper) SendCoinsFromAccountToModule(ctx types.Context, senderAddr types.AccAddress, recipientModule string, amt types.Coins) error { m.ctrl.T.Helper() @@ -183,20 +197,6 @@ func (mr *MockBankKeeperMockRecorder) SendCoinsFromModuleToModule(ctx, senderMod return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoinsFromModuleToModule", reflect.TypeOf((*MockBankKeeper)(nil).SendCoinsFromModuleToModule), ctx, senderModule, recipientModule, amt) } -// SendCoins mocks base method. -func (m *MockBankKeeper) SendCoins(ctx types.Context, fromAddr types.AccAddress, toAddr types.AccAddress, amt types.Coins) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SendCoins", ctx, fromAddr, toAddr, amt) - ret0, _ := ret[0].(error) - return ret0 -} - -// SendCoins indicates an expected call of SendCoins. -func (mr *MockBankKeeperMockRecorder) SendCoins(ctx, fromAddr, toAddr, amt interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoins", reflect.TypeOf((*MockBankKeeper)(nil).SendCoins), ctx, fromAddr, toAddr, amt) -} - // SpendableCoins mocks base method. func (m *MockBankKeeper) SpendableCoins(ctx types.Context, addr types.AccAddress) types.Coins { m.ctrl.T.Helper() @@ -276,6 +276,20 @@ func (mr *MockStakingKeeperMockRecorder) GetAllSDKDelegations(ctx interface{}) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllSDKDelegations", reflect.TypeOf((*MockStakingKeeper)(nil).GetAllSDKDelegations), ctx) } +// GetAllTokenizeShareRecords mocks base method. +func (m *MockStakingKeeper) GetAllTokenizeShareRecords(ctx types.Context) []types1.TokenizeShareRecord { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllTokenizeShareRecords", ctx) + ret0, _ := ret[0].([]types1.TokenizeShareRecord) + return ret0 +} + +// GetAllTokenizeShareRecords indicates an expected call of GetAllTokenizeShareRecords. +func (mr *MockStakingKeeperMockRecorder) GetAllTokenizeShareRecords(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllTokenizeShareRecords", reflect.TypeOf((*MockStakingKeeper)(nil).GetAllTokenizeShareRecords), ctx) +} + // GetAllValidators mocks base method. func (m *MockStakingKeeper) GetAllValidators(ctx types.Context) []types1.Validator { m.ctrl.T.Helper() @@ -290,6 +304,35 @@ func (mr *MockStakingKeeperMockRecorder) GetAllValidators(ctx interface{}) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllValidators", reflect.TypeOf((*MockStakingKeeper)(nil).GetAllValidators), ctx) } +// GetTokenizeShareRecord mocks base method. +func (m *MockStakingKeeper) GetTokenizeShareRecord(ctx types.Context, id uint64) (types1.TokenizeShareRecord, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTokenizeShareRecord", ctx, id) + ret0, _ := ret[0].(types1.TokenizeShareRecord) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTokenizeShareRecord indicates an expected call of GetTokenizeShareRecord. +func (mr *MockStakingKeeperMockRecorder) GetTokenizeShareRecord(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTokenizeShareRecord", reflect.TypeOf((*MockStakingKeeper)(nil).GetTokenizeShareRecord), ctx, id) +} + +// GetTokenizeShareRecordsByOwner mocks base method. +func (m *MockStakingKeeper) GetTokenizeShareRecordsByOwner(ctx types.Context, owner types.AccAddress) []types1.TokenizeShareRecord { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTokenizeShareRecordsByOwner", ctx, owner) + ret0, _ := ret[0].([]types1.TokenizeShareRecord) + return ret0 +} + +// GetTokenizeShareRecordsByOwner indicates an expected call of GetTokenizeShareRecordsByOwner. +func (mr *MockStakingKeeperMockRecorder) GetTokenizeShareRecordsByOwner(ctx, owner interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTokenizeShareRecordsByOwner", reflect.TypeOf((*MockStakingKeeper)(nil).GetTokenizeShareRecordsByOwner), ctx, owner) +} + // IterateDelegations mocks base method. func (m *MockStakingKeeper) IterateDelegations(ctx types.Context, delegator types.AccAddress, fn func(int64, types1.DelegationI) bool) { m.ctrl.T.Helper() @@ -342,49 +385,6 @@ func (mr *MockStakingKeeperMockRecorder) ValidatorByConsAddr(arg0, arg1 interfac return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidatorByConsAddr", reflect.TypeOf((*MockStakingKeeper)(nil).ValidatorByConsAddr), arg0, arg1) } -// GetTokenizeShareRecordsByOwner mocks base method. -func (m *MockStakingKeeper) GetTokenizeShareRecordsByOwner(ctx types.Context, owner types.AccAddress) (tokenizeShareRecords []types1.TokenizeShareRecord) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTokenizeShareRecordsByOwner", ctx, owner) - ret0, _ := ret[0].([]types1.TokenizeShareRecord) - return ret0 -} - -// GetTokenizeShareRecordsByOwner indicates an expected call of GetTokenizeShareRecordsByOwner. -func (mr *MockStakingKeeperMockRecorder) GetTokenizeShareRecordsByOwner(ctx, owner interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTokenizeShareRecordsByOwner", reflect.TypeOf((*MockStakingKeeper)(nil).GetTokenizeShareRecordsByOwner), ctx, owner) -} - -// GetTokenizeShareRecord mocks base method. -func (m *MockStakingKeeper) GetTokenizeShareRecord(ctx types.Context, id uint64) (tokenizeShareRecord types1.TokenizeShareRecord, err error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTokenizeShareRecord", ctx, id) - ret0, _ := ret[0].(types1.TokenizeShareRecord) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetTokenizeShareRecord indicates an expected call of GetTokenizeShareRecord. -func (mr *MockStakingKeeperMockRecorder) GetTokenizeShareRecord(ctx, id interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTokenizeShareRecord", reflect.TypeOf((*MockStakingKeeper)(nil).GetTokenizeShareRecord), ctx, id) -} - -// GetAllTokenizeShareRecords mocks base method. -func (m *MockStakingKeeper) GetAllTokenizeShareRecords(ctx types.Context) (tokenizeShareRecords []types1.TokenizeShareRecord) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAllTokenizeShareRecords", ctx) - ret0, _ := ret[0].([]types1.TokenizeShareRecord) - return ret0 -} - -// GetAllTokenizeShareRecords indicates an expected call of GetAllTokenizeShareRecords. -func (mr *MockStakingKeeperMockRecorder) GetAllTokenizeShareRecords(ctx interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllTokenizeShareRecords", reflect.TypeOf((*MockStakingKeeper)(nil).GetAllTokenizeShareRecords), ctx) -} - // MockStakingHooks is a mock of StakingHooks interface. type MockStakingHooks struct { ctrl *gomock.Controller diff --git a/x/slashing/keeper/hooks.go b/x/slashing/keeper/hooks.go index bdb2f38c1439..b0e10ab33ab9 100644 --- a/x/slashing/keeper/hooks.go +++ b/x/slashing/keeper/hooks.go @@ -90,3 +90,7 @@ func (h Hooks) BeforeValidatorSlashed(_ sdk.Context, _ sdk.ValAddress, _ sdk.Dec func (h Hooks) AfterUnbondingInitiated(_ sdk.Context, _ uint64) error { return nil } + +func (h Hooks) BeforeTokenizeShareRecordRemoved(ctx sdk.Context, recordID uint64) error { + return nil +} diff --git a/x/staking/abci.go b/x/staking/abci.go index 1912beb99747..6b14b025a514 100644 --- a/x/staking/abci.go +++ b/x/staking/abci.go @@ -17,6 +17,7 @@ func BeginBlocker(ctx sdk.Context, k *keeper.Keeper) { defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyBeginBlocker) k.TrackHistoricalInfo(ctx) + k.RemoveExpiredTokenizeShareLocks(ctx, ctx.BlockTime()) } // Called every block, update validator set diff --git a/x/staking/client/cli/query.go b/x/staking/client/cli/query.go index 0982296161e4..1de70b7cc8ea 100644 --- a/x/staking/client/cli/query.go +++ b/x/staking/client/cli/query.go @@ -39,6 +39,14 @@ func GetQueryCmd() *cobra.Command { GetCmdQueryHistoricalInfo(), GetCmdQueryParams(), GetCmdQueryPool(), + GetCmdQueryTokenizeShareRecordByID(), + GetCmdQueryTokenizeShareRecordByDenom(), + GetCmdQueryTokenizeShareRecordsOwned(), + GetCmdQueryAllTokenizeShareRecords(), + GetCmdQueryLastTokenizeShareRecordID(), + GetCmdQueryTotalTokenizeSharedAssets(), + GetCmdQueryTokenizeShareLockInfo(), + GetCmdQueryTotalLiquidStaked(), ) return stakingQueryCmd @@ -744,3 +752,326 @@ $ %s query staking params return cmd } + +// GetCmdQueryTokenizeShareRecordById implements the query for individual tokenize share record information by share by id +func GetCmdQueryTokenizeShareRecordByID() *cobra.Command { + cmd := &cobra.Command{ + Use: "tokenize-share-record-by-id [id]", + Args: cobra.ExactArgs(1), + Short: "Query individual tokenize share record information by share by id", + Long: strings.TrimSpace( + fmt.Sprintf(`Query individual tokenize share record information by share by id. + +Example: +$ %s query staking tokenize-share-record-by-id [id] +`, + version.AppName, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + queryClient := types.NewQueryClient(clientCtx) + + id, err := strconv.Atoi(args[0]) + if err != nil { + return err + } + + res, err := queryClient.TokenizeShareRecordById(cmd.Context(), &types.QueryTokenizeShareRecordByIdRequest{ + Id: uint64(id), + }) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} + +// GetCmdQueryTokenizeShareRecordByDenom implements the query for individual tokenize share record information by share denom +func GetCmdQueryTokenizeShareRecordByDenom() *cobra.Command { + cmd := &cobra.Command{ + Use: "tokenize-share-record-by-denom", + Args: cobra.ExactArgs(1), + Short: "Query individual tokenize share record information by share denom", + Long: strings.TrimSpace( + fmt.Sprintf(`Query individual tokenize share record information by share denom. + +Example: +$ %s query staking tokenize-share-record-by-denom +`, + version.AppName, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + queryClient := types.NewQueryClient(clientCtx) + + res, err := queryClient.TokenizeShareRecordByDenom(cmd.Context(), &types.QueryTokenizeShareRecordByDenomRequest{ + Denom: args[0], + }) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} + +// GetCmdQueryTokenizeShareRecordsOwned implements the query tokenize share records by address +func GetCmdQueryTokenizeShareRecordsOwned() *cobra.Command { + cmd := &cobra.Command{ + Use: "tokenize-share-records-owned", + Args: cobra.ExactArgs(1), + Short: "Query tokenize share records by address", + Long: strings.TrimSpace( + fmt.Sprintf(`Query tokenize share records by address. + +Example: +$ %s query staking tokenize-share-records-owned [owner] +`, + version.AppName, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + queryClient := types.NewQueryClient(clientCtx) + + owner, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + + res, err := queryClient.TokenizeShareRecordsOwned(cmd.Context(), &types.QueryTokenizeShareRecordsOwnedRequest{ + Owner: owner.String(), + }) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} + +// GetCmdQueryAllTokenizeShareRecords implements the query for all tokenize share records +func GetCmdQueryAllTokenizeShareRecords() *cobra.Command { + cmd := &cobra.Command{ + Use: "all-tokenize-share-records", + Args: cobra.NoArgs, + Short: "Query for all tokenize share records", + Long: strings.TrimSpace( + fmt.Sprintf(`Query for all tokenize share records. + +Example: +$ %s query staking all-tokenize-share-records +`, + version.AppName, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + queryClient := types.NewQueryClient(clientCtx) + + pageReq, err := client.ReadPageRequest(cmd.Flags()) + if err != nil { + return err + } + + params := &types.QueryAllTokenizeShareRecordsRequest{ + Pagination: pageReq, + } + + res, err := queryClient.AllTokenizeShareRecords(cmd.Context(), params) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + flags.AddPaginationFlagsToCmd(cmd, "tokenize share records") + + return cmd +} + +// GetCmdQueryLastTokenizeShareRecordId implements the query for last tokenize share record id +func GetCmdQueryLastTokenizeShareRecordID() *cobra.Command { + cmd := &cobra.Command{ + Use: "last-tokenize-share-record-id", + Args: cobra.NoArgs, + Short: "Query for last tokenize share record id", + Long: strings.TrimSpace( + fmt.Sprintf(`Query for last tokenize share record id. + +Example: +$ %s query staking last-tokenize-share-record-id +`, + version.AppName, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + queryClient := types.NewQueryClient(clientCtx) + + res, err := queryClient.LastTokenizeShareRecordId(cmd.Context(), &types.QueryLastTokenizeShareRecordIdRequest{}) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} + +// GetCmdQueryTotalTokenizeSharedAssets implements the query for total tokenized staked assets +func GetCmdQueryTotalTokenizeSharedAssets() *cobra.Command { + cmd := &cobra.Command{ + Use: "total-tokenize-share-assets", + Args: cobra.NoArgs, + Short: "Query for total tokenized staked assets", + Long: strings.TrimSpace( + fmt.Sprintf(`Query for total tokenized staked assets. + +Example: +$ %s query staking total-tokenize-share-assets +`, + version.AppName, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + queryClient := types.NewQueryClient(clientCtx) + + res, err := queryClient.TotalTokenizeSharedAssets(cmd.Context(), &types.QueryTotalTokenizeSharedAssetsRequest{}) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} + +// GetCmdQueryTotalLiquidStaked implements the query for total liquid staked tokens +func GetCmdQueryTotalLiquidStaked() *cobra.Command { + cmd := &cobra.Command{ + Use: "total-liquid-staked", + Args: cobra.NoArgs, + Short: "Query for total liquid staked tokens", + Long: strings.TrimSpace( + fmt.Sprintf(`Query for total number of liquid staked tokens. +Liquid staked tokens are identified as either a tokenized delegation, +or tokens owned by an interchain account. +Example: +$ %s query staking total-liquid-staked +`, + version.AppName, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + queryClient := types.NewQueryClient(clientCtx) + + res, err := queryClient.TotalLiquidStaked(cmd.Context(), &types.QueryTotalLiquidStaked{}) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} + +// GetCmdQueryTokenizeShareLockInfo returns the tokenize share lock status for a user +func GetCmdQueryTokenizeShareLockInfo() *cobra.Command { + bech32PrefixAccAddr := sdk.GetConfig().GetBech32AccountAddrPrefix() + + cmd := &cobra.Command{ + Use: "tokenize-share-lock-info [address]", + Args: cobra.ExactArgs(1), + Short: "Query tokenize share lock information", + Long: strings.TrimSpace( + fmt.Sprintf(`Query the status of a tokenize share lock for a given account +Example: +$ %s query staking tokenize-share-lock-info %s1gghjut3ccd8ay0zduzj64hwre2fxs9ldmqhffj +`, + version.AppName, bech32PrefixAccAddr, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + queryClient := types.NewQueryClient(clientCtx) + + address := args[0] + if _, err := sdk.AccAddressFromBech32(address); err != nil { + return err + } + + res, err := queryClient.TokenizeShareLockInfo( + cmd.Context(), + &types.QueryTokenizeShareLockInfo{Address: address}, + ) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + return cmd +} diff --git a/x/staking/client/cli/tx.go b/x/staking/client/cli/tx.go index ceebbe40a54b..d99a3b3796c4 100644 --- a/x/staking/client/cli/tx.go +++ b/x/staking/client/cli/tx.go @@ -44,7 +44,14 @@ func NewTxCmd() *cobra.Command { NewDelegateCmd(), NewRedelegateCmd(), NewUnbondCmd(), + NewUnbondValidatorCmd(), NewCancelUnbondingDelegation(), + NewTokenizeSharesCmd(), + NewRedeemTokensCmd(), + NewTransferTokenizeShareRecordCmd(), + NewDisableTokenizeShares(), + NewEnableTokenizeShares(), + NewValidatorBondCmd(), ) return stakingTxCmd @@ -272,6 +279,37 @@ $ %s tx staking unbond %s1gghjut3ccd8ay0zduzj64hwre2fxs9ldmqhffj 100stake --from return cmd } +func NewUnbondValidatorCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "unbond-validator", + Short: "Unbond a validator", + Args: cobra.ExactArgs(0), + Long: strings.TrimSpace( + fmt.Sprintf(`Unbond a validator. + +Example: +$ %s tx staking unbond-validator --from mykey +`, + version.AppName, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + msg := types.NewMsgUnbondValidator(sdk.ValAddress(clientCtx.GetFromAddress())) + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + // NewCancelUnbondingDelegation returns a CLI command handler for creating a MsgCancelUnbondingDelegation transaction. func NewCancelUnbondingDelegation() *cobra.Command { bech32PrefixValAddr := sdk.GetConfig().GetBech32ValidatorAddrPrefix() @@ -575,3 +613,251 @@ func BuildCreateValidatorMsg(clientCtx client.Context, config TxCreateValidatorC return txBldr, msg, nil } + +// NewTokenizeSharesCmd defines a command for tokenizing shares from a validator. +func NewTokenizeSharesCmd() *cobra.Command { + bech32PrefixValAddr := sdk.GetConfig().GetBech32ValidatorAddrPrefix() + bech32PrefixAccAddr := sdk.GetConfig().GetBech32AccountAddrPrefix() + + cmd := &cobra.Command{ + Use: "tokenize-share [validator-addr] [amount] [rewardOwner]", + Short: "Tokenize delegation to share tokens", + Args: cobra.ExactArgs(3), + Long: strings.TrimSpace( + fmt.Sprintf(`Tokenize delegation to share tokens. + +Example: +$ %s tx staking tokenize-share %s1gghjut3ccd8ay0zduzj64hwre2fxs9ldmqhffj 100stake %s1gghjut3ccd8ay0zduzj64hwre2fxs9ldmqhffj --from mykey +`, + version.AppName, bech32PrefixValAddr, bech32PrefixAccAddr, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + delAddr := clientCtx.GetFromAddress() + valAddr, err := sdk.ValAddressFromBech32(args[0]) + if err != nil { + return err + } + + amount, err := sdk.ParseCoinNormalized(args[1]) + if err != nil { + return err + } + + rewardOwner, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + + msg := &types.MsgTokenizeShares{ + DelegatorAddress: delAddr.String(), + ValidatorAddress: valAddr.String(), + Amount: amount, + TokenizedShareOwner: rewardOwner.String(), + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + +// NewRedeemTokensCmd defines a command for redeeming tokens from a validator for shares. +func NewRedeemTokensCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "redeem-tokens [amount]", + Short: "Redeem specified amount of share tokens to delegation", + Args: cobra.ExactArgs(1), + Long: strings.TrimSpace( + fmt.Sprintf(`Redeem specified amount of share tokens to delegation. + +Example: +$ %s tx staking redeem-tokens 100sharetoken --from mykey +`, + version.AppName, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + delAddr := clientCtx.GetFromAddress() + + amount, err := sdk.ParseCoinNormalized(args[0]) + if err != nil { + return err + } + + msg := &types.MsgRedeemTokensForShares{ + DelegatorAddress: delAddr.String(), + Amount: amount, + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + +// NewTransferTokenizeShareRecordCmd defines a command to transfer ownership of TokenizeShareRecord +func NewTransferTokenizeShareRecordCmd() *cobra.Command { + bech32PrefixAccAddr := sdk.GetConfig().GetBech32AccountAddrPrefix() + + cmd := &cobra.Command{ + Use: "transfer-tokenize-share-record [record-id] [new-owner]", + Short: "Transfer ownership of TokenizeShareRecord", + Args: cobra.ExactArgs(2), + Long: strings.TrimSpace( + fmt.Sprintf(`Transfer ownership of TokenizeShareRecord. + +Example: +$ %s tx staking transfer-tokenize-share-record 1 %s1gghjut3ccd8ay0zduzj64hwre2fxs9ldmqhffj --from mykey +`, + version.AppName, bech32PrefixAccAddr, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + recordID, err := strconv.Atoi(args[0]) + if err != nil { + return err + } + + ownerAddr, err := sdk.AccAddressFromBech32(args[1]) + if err != nil { + return err + } + + msg := &types.MsgTransferTokenizeShareRecord{ + Sender: clientCtx.GetFromAddress().String(), + TokenizeShareRecordId: uint64(recordID), + NewOwner: ownerAddr.String(), + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + +// NewDisableTokenizeShares defines a command to disable tokenization for an address +func NewDisableTokenizeShares() *cobra.Command { + cmd := &cobra.Command{ + Use: "disable-tokenize-shares", + Short: "Disable tokenization of shares", + Args: cobra.ExactArgs(0), + Long: strings.TrimSpace( + fmt.Sprintf(`Disables the tokenization of shares for an address. The account +must explicitly re-enable if they wish to tokenize again, at which point they must wait +the chain's unbonding period. + +Example: +$ %s tx staking disable-tokenize-shares --from mykey +`, version.AppName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + msg := &types.MsgDisableTokenizeShares{ + DelegatorAddress: clientCtx.GetFromAddress().String(), + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + +// NewEnableTokenizeShares defines a command to re-enable tokenization for an address +func NewEnableTokenizeShares() *cobra.Command { + cmd := &cobra.Command{ + Use: "enable-tokenize-shares", + Short: "Enable tokenization of shares", + Args: cobra.ExactArgs(0), + Long: strings.TrimSpace( + fmt.Sprintf(`Enables the tokenization of shares for an address after +it had been disable. This transaction queues the enablement of tokenization, but +the address must wait 1 unbonding period from the time of this transaction before +tokenization is permitted. + +Example: +$ %s tx staking enable-tokenize-shares --from mykey +`, version.AppName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + msg := &types.MsgEnableTokenizeShares{ + DelegatorAddress: clientCtx.GetFromAddress().String(), + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + +// NewValidatorBondCmd defines a command to mark a delegation as a validator self bond +func NewValidatorBondCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "validator-bond [validator]", + Short: "Mark a delegation as a validator self-bond", + Args: cobra.ExactArgs(1), + Long: strings.TrimSpace( + fmt.Sprintf(`Mark a delegation as a validator self-bond. + +Example: +$ %s tx staking validator-bond cosmosvaloper13h5xdxhsdaugwdrkusf8lkgu406h8t62jkqv3h --from mykey +`, + version.AppName, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + msg := &types.MsgValidatorBond{ + DelegatorAddress: clientCtx.GetFromAddress().String(), + ValidatorAddress: args[0], + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} diff --git a/x/staking/keeper/liquid_stake.go b/x/staking/keeper/liquid_stake.go new file mode 100644 index 000000000000..b9b5280bd17c --- /dev/null +++ b/x/staking/keeper/liquid_stake.go @@ -0,0 +1,430 @@ +package keeper + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +// SetTotalLiquidStakedTokens stores the total outstanding tokens owned by a liquid staking provider +func (k Keeper) SetTotalLiquidStakedTokens(ctx sdk.Context, tokens sdk.Int) { + store := ctx.KVStore(k.storeKey) + + tokensBz, err := tokens.Marshal() + if err != nil { + panic(err) + } + + store.Set(types.TotalLiquidStakedTokensKey, tokensBz) +} + +// GetTotalLiquidStakedTokens returns the total outstanding tokens owned by a liquid staking provider +// Returns zero if the total liquid stake amount has not been initialized +func (k Keeper) GetTotalLiquidStakedTokens(ctx sdk.Context) sdk.Int { + store := ctx.KVStore(k.storeKey) + tokensBz := store.Get(types.TotalLiquidStakedTokensKey) + + if tokensBz == nil { + return sdk.ZeroInt() + } + + var tokens sdk.Int + if err := tokens.Unmarshal(tokensBz); err != nil { + panic(err) + } + + return tokens +} + +// Checks if an account associated with a given delegation is related to liquid staking +// +// This is determined by checking if the account has a 32-length address +// which will identify the following scenarios: +// - An account has tokenized their shares, and thus the delegation is +// owned by the tokenize share record module account +// - A liquid staking provider is delegating through an ICA account +// +// Both ICA accounts and tokenize share record module accounts have 32-length addresses +// NOTE: This will have to be refactored before adapting it to chains beyond gaia +// as other chains may have 32-length addresses that are not related to the above scenarios +func (k Keeper) DelegatorIsLiquidStaker(delegatorAddress sdk.AccAddress) bool { + return len(delegatorAddress) == 32 +} + +// CheckExceedsGlobalLiquidStakingCap checks if a liquid delegation would cause the +// global liquid staking cap to be exceeded +// A liquid delegation is defined as either tokenized shares, or a delegation from an ICA Account +// The total stake is determined by the balance of the bonded pool +// If the delegation's shares are already bonded (e.g. in the event of a tokenized share) +// the tokens are already included in the bonded pool +// If the delegation's shares are not bonded (e.g. normal delegation), +// we need to add the tokens to the current bonded pool balance to get the total staked +func (k Keeper) CheckExceedsGlobalLiquidStakingCap(ctx sdk.Context, tokens sdk.Int, sharesAlreadyBonded bool) bool { + liquidStakingCap := k.GlobalLiquidStakingCap(ctx) + liquidStakedAmount := k.GetTotalLiquidStakedTokens(ctx) + + // Determine the total stake from the balance of the bonded pool + // If this is not a tokenized delegation, we need to add the tokens to the pool balance since + // they would not have been counted yet + // If this is for a tokenized delegation, the tokens are already included in the pool balance + totalStakedAmount := k.TotalBondedTokens(ctx) + if !sharesAlreadyBonded { + totalStakedAmount = totalStakedAmount.Add(tokens) + } + + // Calculate the percentage of stake that is liquid + updatedLiquidStaked := liquidStakedAmount.Add(tokens).ToLegacyDec() + liquidStakePercent := updatedLiquidStaked.Quo(totalStakedAmount.ToLegacyDec()) + + return liquidStakePercent.GT(liquidStakingCap) +} + +// CheckExceedsValidatorBondCap checks if a liquid delegation to a validator would cause +// the liquid shares to exceed the validator bond factor +// A liquid delegation is defined as either tokenized shares, or a delegation from an ICA Account +// Returns true if the cap is exceeded +func (k Keeper) CheckExceedsValidatorBondCap(ctx sdk.Context, validator types.Validator, shares sdk.Dec) bool { + validatorBondFactor := k.ValidatorBondFactor(ctx) + if validatorBondFactor.Equal(types.ValidatorBondCapDisabled) { + return false + } + maxValLiquidShares := validator.ValidatorBondShares.Mul(validatorBondFactor) + return validator.LiquidShares.Add(shares).GT(maxValLiquidShares) +} + +// CheckExceedsValidatorLiquidStakingCap checks if a liquid delegation could cause the +// total liuquid shares to exceed the liquid staking cap +// A liquid delegation is defined as either tokenized shares, or a delegation from an ICA Account +// Returns true if the cap is exceeded +func (k Keeper) CheckExceedsValidatorLiquidStakingCap(ctx sdk.Context, validator types.Validator, shares sdk.Dec) bool { + updatedLiquidShares := validator.LiquidShares.Add(shares) + updatedTotalShares := validator.DelegatorShares.Add(shares) + + liquidStakePercent := updatedLiquidShares.Quo(updatedTotalShares) + liquidStakingCap := k.ValidatorLiquidStakingCap(ctx) + + return liquidStakePercent.GT(liquidStakingCap) +} + +// SafelyIncreaseTotalLiquidStakedTokens increments the total liquid staked tokens +// if the global cap is not surpassed by this delegation +// +// The percentage of liquid staked tokens must be less than the GlobalLiquidStakingCap: +// (TotalLiquidStakedTokens / TotalStakedTokens) <= GlobalLiquidStakingCap +func (k Keeper) SafelyIncreaseTotalLiquidStakedTokens(ctx sdk.Context, amount sdk.Int, sharesAlreadyBonded bool) error { + if k.CheckExceedsGlobalLiquidStakingCap(ctx, amount, sharesAlreadyBonded) { + return types.ErrGlobalLiquidStakingCapExceeded + } + + k.SetTotalLiquidStakedTokens(ctx, k.GetTotalLiquidStakedTokens(ctx).Add(amount)) + return nil +} + +// DecreaseTotalLiquidStakedTokens decrements the total liquid staked tokens +func (k Keeper) DecreaseTotalLiquidStakedTokens(ctx sdk.Context, amount sdk.Int) error { + totalLiquidStake := k.GetTotalLiquidStakedTokens(ctx) + if amount.GT(totalLiquidStake) { + return types.ErrTotalLiquidStakedUnderflow + } + k.SetTotalLiquidStakedTokens(ctx, totalLiquidStake.Sub(amount)) + return nil +} + +// SafelyIncreaseValidatorLiquidShares increments the liquid shares on a validator, if: +// the validator bond factor and validator liquid staking cap will not be exceeded by this delegation +// +// The percentage of validator liquid shares must be less than the ValidatorLiquidStakingCap, +// and the total liquid staked shares cannot exceed the validator bond cap +// 1) (TotalLiquidStakedTokens / TotalStakedTokens) <= ValidatorLiquidStakingCap +// 2) LiquidShares <= (ValidatorBondShares * ValidatorBondFactor) +func (k Keeper) SafelyIncreaseValidatorLiquidShares(ctx sdk.Context, valAddress sdk.ValAddress, shares sdk.Dec) (types.Validator, error) { + validator, found := k.GetValidator(ctx, valAddress) + if !found { + return validator, types.ErrNoValidatorFound + } + + // Confirm the validator bond factor and validator liquid staking cap will not be exceeded + if k.CheckExceedsValidatorBondCap(ctx, validator, shares) { + return validator, types.ErrInsufficientValidatorBondShares + } + if k.CheckExceedsValidatorLiquidStakingCap(ctx, validator, shares) { + return validator, types.ErrValidatorLiquidStakingCapExceeded + } + + // Increment the validator's liquid shares + validator.LiquidShares = validator.LiquidShares.Add(shares) + k.SetValidator(ctx, validator) + + return validator, nil +} + +// DecreaseValidatorLiquidShares decrements the liquid shares on a validator +func (k Keeper) DecreaseValidatorLiquidShares(ctx sdk.Context, valAddress sdk.ValAddress, shares sdk.Dec) (types.Validator, error) { + validator, found := k.GetValidator(ctx, valAddress) + if !found { + return validator, types.ErrNoValidatorFound + } + + if shares.GT(validator.LiquidShares) { + return validator, types.ErrValidatorLiquidSharesUnderflow + } + + validator.LiquidShares = validator.LiquidShares.Sub(shares) + k.SetValidator(ctx, validator) + + return validator, nil +} + +// Increase validator bond shares increments the validator's self bond +// in the event that the delegation amount on a validator bond delegation is increased +func (k Keeper) IncreaseValidatorBondShares(ctx sdk.Context, valAddress sdk.ValAddress, shares sdk.Dec) error { + validator, found := k.GetValidator(ctx, valAddress) + if !found { + return types.ErrNoValidatorFound + } + + validator.ValidatorBondShares = validator.ValidatorBondShares.Add(shares) + k.SetValidator(ctx, validator) + + return nil +} + +// SafelyDecreaseValidatorBond decrements the validator's self bond +// so long as it will not cause the current delegations to exceed the threshold +// set by validator bond factor +func (k Keeper) SafelyDecreaseValidatorBond(ctx sdk.Context, valAddress sdk.ValAddress, shares sdk.Dec) error { + validator, found := k.GetValidator(ctx, valAddress) + if !found { + return types.ErrNoValidatorFound + } + + // Check if the decreased self bond will cause the validator bond threshold to be exceeded + validatorBondFactor := k.ValidatorBondFactor(ctx) + validatorBondEnabled := !validatorBondFactor.Equal(types.ValidatorBondCapDisabled) + maxValTotalShare := validator.ValidatorBondShares.Sub(shares).Mul(validatorBondFactor) + + if validatorBondEnabled && validator.LiquidShares.GT(maxValTotalShare) { + return types.ErrInsufficientValidatorBondShares + } + + // Decrement the validator's self bond + validator.ValidatorBondShares = validator.ValidatorBondShares.Sub(shares) + k.SetValidator(ctx, validator) + + return nil +} + +// Adds a lock that prevents tokenizing shares for an account +// The tokenize share lock store is implemented by keying on the account address +// and storing a timestamp as the value. The timestamp is empty when the lock is +// set and gets populated with the unlock completion time once the unlock has started +func (k Keeper) AddTokenizeSharesLock(ctx sdk.Context, address sdk.AccAddress) { + store := ctx.KVStore(k.storeKey) + key := types.GetTokenizeSharesLockKey(address) + store.Set(key, sdk.FormatTimeBytes(time.Time{})) +} + +// Removes the tokenize share lock for an account to enable tokenizing shares +func (k Keeper) RemoveTokenizeSharesLock(ctx sdk.Context, address sdk.AccAddress) { + store := ctx.KVStore(k.storeKey) + key := types.GetTokenizeSharesLockKey(address) + store.Delete(key) +} + +// Updates the timestamp associated with a lock to the time at which the lock expires +func (k Keeper) SetTokenizeSharesUnlockTime(ctx sdk.Context, address sdk.AccAddress, completionTime time.Time) { + store := ctx.KVStore(k.storeKey) + key := types.GetTokenizeSharesLockKey(address) + store.Set(key, sdk.FormatTimeBytes(completionTime)) +} + +// Checks if there is currently a tokenize share lock for a given account +// Returns the status indicating whether the account is locked, unlocked, +// or as a lock expiring. If the lock is expiring, the expiration time is returned +func (k Keeper) GetTokenizeSharesLock(ctx sdk.Context, address sdk.AccAddress) (status types.TokenizeShareLockStatus, unlockTime time.Time) { + store := ctx.KVStore(k.storeKey) + key := types.GetTokenizeSharesLockKey(address) + bz := store.Get(key) + if len(bz) == 0 { + return types.TOKENIZE_SHARE_LOCK_STATUS_UNLOCKED, time.Time{} + } + unlockTime, err := sdk.ParseTimeBytes(bz) + if err != nil { + panic(err) + } + if unlockTime.IsZero() { + return types.TOKENIZE_SHARE_LOCK_STATUS_LOCKED, time.Time{} + } + return types.TOKENIZE_SHARE_LOCK_STATUS_LOCK_EXPIRING, unlockTime +} + +// Returns all tokenize share locks +func (k Keeper) GetAllTokenizeSharesLocks(ctx sdk.Context) (tokenizeShareLocks []types.TokenizeShareLock) { + store := ctx.KVStore(k.storeKey) + + iterator := sdk.KVStorePrefixIterator(store, types.TokenizeSharesLockPrefix) + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + addressBz := iterator.Key()[2:] // remove prefix bytes and address length + unlockTime, err := sdk.ParseTimeBytes(iterator.Value()) + if err != nil { + panic(err) + } + + var status types.TokenizeShareLockStatus + if unlockTime.IsZero() { + status = types.TOKENIZE_SHARE_LOCK_STATUS_LOCKED + } else { + status = types.TOKENIZE_SHARE_LOCK_STATUS_LOCK_EXPIRING + } + + bechPrefix := sdk.GetConfig().GetBech32AccountAddrPrefix() + lock := types.TokenizeShareLock{ + Address: sdk.MustBech32ifyAddressBytes(bechPrefix, addressBz), + Status: status.String(), + CompletionTime: unlockTime, + } + + tokenizeShareLocks = append(tokenizeShareLocks, lock) + } + + return tokenizeShareLocks +} + +// Stores a list of addresses pending tokenize share unlocking at the same time +func (k Keeper) SetPendingTokenizeShareAuthorizations(ctx sdk.Context, completionTime time.Time, authorizations types.PendingTokenizeShareAuthorizations) { + store := ctx.KVStore(k.storeKey) + timeKey := types.GetTokenizeShareAuthorizationTimeKey(completionTime) + bz := k.cdc.MustMarshal(&authorizations) + store.Set(timeKey, bz) +} + +// Returns a list of addresses pending tokenize share unlocking at the same time +func (k Keeper) GetPendingTokenizeShareAuthorizations(ctx sdk.Context, completionTime time.Time) types.PendingTokenizeShareAuthorizations { + store := ctx.KVStore(k.storeKey) + + timeKey := types.GetTokenizeShareAuthorizationTimeKey(completionTime) + bz := store.Get(timeKey) + + authorizations := types.PendingTokenizeShareAuthorizations{Addresses: []string{}} + if len(bz) == 0 { + return authorizations + } + k.cdc.MustUnmarshal(bz, &authorizations) + + return authorizations +} + +// Inserts the address into a queue where it will sit for 1 unbonding period +// before the tokenize share lock is removed +// Returns the completion time +func (k Keeper) QueueTokenizeSharesAuthorization(ctx sdk.Context, address sdk.AccAddress) time.Time { + params := k.GetParams(ctx) + completionTime := ctx.BlockTime().Add(params.UnbondingTime) + + // Append the address to the list of addresses that also unlock at this time + authorizations := k.GetPendingTokenizeShareAuthorizations(ctx, completionTime) + authorizations.Addresses = append(authorizations.Addresses, address.String()) + + k.SetPendingTokenizeShareAuthorizations(ctx, completionTime, authorizations) + k.SetTokenizeSharesUnlockTime(ctx, address, completionTime) + + return completionTime +} + +// Cancels a pending tokenize share authorization by removing the lock from the queue +func (k Keeper) CancelTokenizeShareLockExpiration(ctx sdk.Context, address sdk.AccAddress, completionTime time.Time) { + authorizations := k.GetPendingTokenizeShareAuthorizations(ctx, completionTime) + + updatedAddresses := []string{} + for _, expiringAddress := range authorizations.Addresses { + if address.String() != expiringAddress { + updatedAddresses = append(updatedAddresses, expiringAddress) + } + } + + authorizations.Addresses = updatedAddresses + k.SetPendingTokenizeShareAuthorizations(ctx, completionTime, authorizations) +} + +// Unlocks all queued tokenize share authorizations that have matured +// (i.e. have waited the full unbonding period) +func (k Keeper) RemoveExpiredTokenizeShareLocks(ctx sdk.Context, blockTime time.Time) (unlockedAddresses []string) { + store := ctx.KVStore(k.storeKey) + + // iterators all time slices from time 0 until the current block time + prefixEnd := sdk.InclusiveEndBytes(types.GetTokenizeShareAuthorizationTimeKey(blockTime)) + iterator := store.Iterator(types.TokenizeSharesUnlockQueuePrefix, prefixEnd) + defer iterator.Close() + + // collect all unlocked addresses + unlockedAddresses = []string{} + for ; iterator.Valid(); iterator.Next() { + authorizations := types.PendingTokenizeShareAuthorizations{} + k.cdc.MustUnmarshal(iterator.Value(), &authorizations) + + for _, addressString := range authorizations.Addresses { + unlockedAddresses = append(unlockedAddresses, addressString) + } + store.Delete(iterator.Key()) + } + + // remove the lock from each unlocked address + for _, unlockedAddress := range unlockedAddresses { + k.RemoveTokenizeSharesLock(ctx, sdk.MustAccAddressFromBech32(unlockedAddress)) + } + + return unlockedAddresses +} + +// Calculates and sets the global liquid staked tokens and liquid shares by validator +// The totals are determined by looping each delegation record and summing the stake +// if the delegator has a 32-length address. Checking for a 32-length address will capture +// ICA accounts, as well as tokenized delegations which are owned by module accounts +// under the hood +// This function must be called in the upgrade handler which onboards LSM +func (k Keeper) RefreshTotalLiquidStaked(ctx sdk.Context) error { + // First reset each validator's liquid shares to 0 + for _, validator := range k.GetAllValidators(ctx) { + validator.LiquidShares = sdk.ZeroDec() + k.SetValidator(ctx, validator) + } + + // Sum up the total liquid tokens and increment each validator's liquid shares + totalLiquidStakedTokens := sdk.ZeroInt() + for _, delegation := range k.GetAllDelegations(ctx) { + delegatorAddress, err := sdk.AccAddressFromBech32(delegation.DelegatorAddress) + if err != nil { + return err + } + + // If the delegator is either an ICA account or a tokenize share module account, + // the delegation should be considered to be associated with liquid staking + // Consequently, the global number of liquid staked tokens, and the total + // liquid shares on the validator should be incremented + if k.DelegatorIsLiquidStaker(delegatorAddress) { + validatorAddress, err := sdk.ValAddressFromBech32(delegation.ValidatorAddress) + if err != nil { + return err + } + validator, found := k.GetValidator(ctx, validatorAddress) + if !found { + return types.ErrNoValidatorFound + } + + liquidShares := delegation.Shares + liquidTokens := validator.TokensFromShares(liquidShares).TruncateInt() + + validator.LiquidShares = validator.LiquidShares.Add(liquidShares) + k.SetValidator(ctx, validator) + + totalLiquidStakedTokens = totalLiquidStakedTokens.Add(liquidTokens) + } + } + + k.SetTotalLiquidStakedTokens(ctx, totalLiquidStakedTokens) + + return nil +} diff --git a/x/staking/keeper/liquid_stake_test.go b/x/staking/keeper/liquid_stake_test.go new file mode 100644 index 000000000000..5717fd7c0640 --- /dev/null +++ b/x/staking/keeper/liquid_stake_test.go @@ -0,0 +1,1207 @@ +package keeper_test + +// import ( +// "fmt" +// "testing" +// "time" + +// testutil "github.com/cosmos/cosmos-sdk/testutil/sims" + +// "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" +// "github.com/cosmos/cosmos-sdk/simapp" +// sdk "github.com/cosmos/cosmos-sdk/types" +// "github.com/cosmos/cosmos-sdk/types/address" +// authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" +// minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" +// "github.com/cosmos/cosmos-sdk/x/staking/types" +// "github.com/stretchr/testify/require" +// ) + +// // Helper function to create a base account from an account name +// // Used to differentiate against liquid staking provider module account +// func createBaseAccount(app *simapp.SimApp, ctx sdk.Context, accountName string) sdk.AccAddress { +// baseAccountAddress := sdk.AccAddress(accountName) +// app.AccountKeeper.SetAccount(ctx, authtypes.NewBaseAccountWithAddress(baseAccountAddress)) +// return baseAccountAddress +// } + +// // Helper function to create 32-length account +// // Used to mock an liquid staking provider's ICA account +// func createICAAccount(app *simapp.SimApp, ctx sdk.Context) sdk.AccAddress { +// icahost := "icahost" +// connectionID := "connection-0" +// portID := icahost + +// moduleAddress := authtypes.NewModuleAddress(icahost) +// icaAddress := sdk.AccAddress(address.Derive(moduleAddress, []byte(connectionID+portID))) + +// account := authtypes.NewBaseAccountWithAddress(icaAddress) +// app.AccountKeeper.SetAccount(ctx, account) + +// return icaAddress +// } + +// // Helper function to create a module account address from a tokenized share +// // Used to mock the delegation owner of a tokenized share +// func createTokenizeShareModuleAccount(recordID uint64) sdk.AccAddress { +// record := types.TokenizeShareRecord{ +// Id: recordID, +// ModuleAccount: fmt.Sprintf("%s%d", types.TokenizeShareModuleAccountPrefix, recordID), +// } +// return record.GetModuleAddress() +// } + +// // Tests Set/Get TotalLiquidStakedTokens +// func (s *KeeperTestSuite) TestTotalLiquidStakedTokens(t *testing.T) { +// ctx, keeper := s.ctx, s.stakingKeeper +// require := s.Require() + +// // Update the total liquid staked +// total := sdk.NewInt(100) +// keeper.SetTotalLiquidStakedTokens(ctx, total) + +// // Confirm it was updated +// require.Equal(t, total, keeper.GetTotalLiquidStakedTokens(ctx), "initial") +// } + +// // Tests Increase/Decrease TotalValidatorLiquidShares +// func (s *KeeperTestSuite) TestValidatorLiquidShares(t *testing.T) { +// ctx, keeper := s.ctx, s.stakingKeeper + +// // Create a validator address +// privKey := secp256k1.GenPrivKey() +// pubKey := privKey.PubKey() +// valAddress := sdk.ValAddress(pubKey.Address()) + +// // Set an initial total +// initial := sdk.NewDec(100) +// validator := types.Validator{ +// OperatorAddress: valAddress.String(), +// LiquidShares: initial, +// } +// keeper.SetValidator(ctx, validator) +// } + +// // Tests DelegatorIsLiquidStaker +// func (s *KeeperTestSuite) TestDelegatorIsLiquidStaker(t *testing.T) { +// ctx, keeper := s.ctx, s.stakingKeeper +// require := s.Require() + +// // Create base and ICA accounts +// baseAccountAddress := createBaseAccount(app, ctx, "base-account") +// icaAccountAddress := createICAAccount(app, ctx) + +// // Only the ICA module account should be considered a liquid staking provider +// require.False(keeper.DelegatorIsLiquidStaker(baseAccountAddress), "base account") +// require.True(keeper.DelegatorIsLiquidStaker(icaAccountAddress), "ICA module account") +// } + +// // Helper function to clear the Bonded pool balances before a unit test +// func clearPoolBalance(t *testing.T, app *simapp.SimApp, ctx sdk.Context) { +// bondDenom := keeper.BondDenom(ctx) +// initialBondedBalance := app.BankKeeper.GetBalance(ctx, app.AccountKeeper.GetModuleAddress(types.BondedPoolName), bondDenom) + +// err := app.BankKeeper.SendCoinsFromModuleToModule(ctx, types.BondedPoolName, minttypes.ModuleName, sdk.NewCoins(initialBondedBalance)) +// require.NoError(t, err, "no error expected when clearing bonded pool balance") +// } + +// // Helper function to fund the Bonded pool balances before a unit test +// func fundPoolBalance(t *testing.T, app *simapp.SimApp, ctx sdk.Context, amount sdk.Int) { +// bondDenom := keeper.BondDenom(ctx) +// bondedPoolCoin := sdk.NewCoin(bondDenom, amount) + +// err := app.BankKeeper.MintCoins(ctx, minttypes.ModuleName, sdk.NewCoins(bondedPoolCoin)) +// require.NoError(t, err, "no error expected when minting") + +// err = app.BankKeeper.SendCoinsFromModuleToModule(ctx, minttypes.ModuleName, types.BondedPoolName, sdk.NewCoins(bondedPoolCoin)) +// require.NoError(t, err, "no error expected when sending tokens to bonded pool") +// } + +// // Tests CheckExceedsGlobalLiquidStakingCap +// func (s *KeeperTestSuite) TestCheckExceedsGlobalLiquidStakingCap(t *testing.T) { +// ctx, keeper := s.ctx, s.stakingKeeper +// require := s.Require() + +// testCases := []struct { +// name string +// globalLiquidCap sdk.Dec +// totalLiquidStake sdk.Int +// totalStake sdk.Int +// newLiquidStake sdk.Int +// tokenizingShares bool +// expectedExceeds bool +// }{ +// { +// // Cap: 10% - Native Delegation - Delegation Below Threshold +// // Total Liquid Stake: 5, Total Stake: 95, New Liquid Stake: 1 +// // => Total Liquid Stake: 5+1=6, Total Stake: 95+1=96 => 6/96 = 6% < 10% cap +// name: "10 percent cap _ native delegation _ delegation below cap", +// globalLiquidCap: sdk.MustNewDecFromStr("0.1"), +// totalLiquidStake: sdk.NewInt(5), +// totalStake: sdk.NewInt(95), +// newLiquidStake: sdk.NewInt(1), +// tokenizingShares: false, +// expectedExceeds: false, +// }, +// { +// // Cap: 10% - Native Delegation - Delegation At Threshold +// // Total Liquid Stake: 5, Total Stake: 95, New Liquid Stake: 5 +// // => Total Liquid Stake: 5+5=10, Total Stake: 95+5=100 => 10/100 = 10% == 10% cap +// name: "10 percent cap _ native delegation _ delegation equals cap", +// globalLiquidCap: sdk.MustNewDecFromStr("0.1"), +// totalLiquidStake: sdk.NewInt(5), +// totalStake: sdk.NewInt(95), +// newLiquidStake: sdk.NewInt(5), +// tokenizingShares: false, +// expectedExceeds: false, +// }, +// { +// // Cap: 10% - Native Delegation - Delegation Exceeds Threshold +// // Total Liquid Stake: 5, Total Stake: 95, New Liquid Stake: 6 +// // => Total Liquid Stake: 5+6=11, Total Stake: 95+6=101 => 11/101 = 11% > 10% cap +// name: "10 percent cap _ native delegation _ delegation exceeds cap", +// globalLiquidCap: sdk.MustNewDecFromStr("0.1"), +// totalLiquidStake: sdk.NewInt(5), +// totalStake: sdk.NewInt(95), +// newLiquidStake: sdk.NewInt(6), +// tokenizingShares: false, +// expectedExceeds: true, +// }, +// { +// // Cap: 20% - Native Delegation - Delegation Below Threshold +// // Total Liquid Stake: 20, Total Stake: 220, New Liquid Stake: 29 +// // => Total Liquid Stake: 20+29=49, Total Stake: 220+29=249 => 49/249 = 19% < 20% cap +// name: "20 percent cap _ native delegation _ delegation below cap", +// globalLiquidCap: sdk.MustNewDecFromStr("0.20"), +// totalLiquidStake: sdk.NewInt(20), +// totalStake: sdk.NewInt(220), +// newLiquidStake: sdk.NewInt(29), +// tokenizingShares: false, +// expectedExceeds: false, +// }, +// { +// // Cap: 20% - Native Delegation - Delegation At Threshold +// // Total Liquid Stake: 20, Total Stake: 220, New Liquid Stake: 30 +// // => Total Liquid Stake: 20+30=50, Total Stake: 220+30=250 => 50/250 = 20% == 20% cap +// name: "20 percent cap _ native delegation _ delegation equals cap", +// globalLiquidCap: sdk.MustNewDecFromStr("0.20"), +// totalLiquidStake: sdk.NewInt(20), +// totalStake: sdk.NewInt(220), +// newLiquidStake: sdk.NewInt(30), +// tokenizingShares: false, +// expectedExceeds: false, +// }, +// { +// // Cap: 20% - Native Delegation - Delegation Exceeds Threshold +// // Total Liquid Stake: 20, Total Stake: 220, New Liquid Stake: 31 +// // => Total Liquid Stake: 20+31=51, Total Stake: 220+31=251 => 51/251 = 21% > 20% cap +// name: "20 percent cap _ native delegation _ delegation exceeds cap", +// globalLiquidCap: sdk.MustNewDecFromStr("0.20"), +// totalLiquidStake: sdk.NewInt(20), +// totalStake: sdk.NewInt(220), +// newLiquidStake: sdk.NewInt(31), +// tokenizingShares: false, +// expectedExceeds: true, +// }, +// { +// // Cap: 50% - Native Delegation - Delegation Below Threshold +// // Total Liquid Stake: 0, Total Stake: 100, New Liquid Stake: 50 +// // => Total Liquid Stake: 0+50=50, Total Stake: 100+50=150 => 50/150 = 33% < 50% cap +// name: "50 percent cap _ native delegation _ delegation below cap", +// globalLiquidCap: sdk.MustNewDecFromStr("0.5"), +// totalLiquidStake: sdk.NewInt(0), +// totalStake: sdk.NewInt(100), +// newLiquidStake: sdk.NewInt(50), +// tokenizingShares: false, +// expectedExceeds: false, +// }, +// { +// // Cap: 50% - Tokenized Delegation - Delegation At Threshold +// // Total Liquid Stake: 0, Total Stake: 100, New Liquid Stake: 50 +// // => 50 / 100 = 50% == 50% cap +// name: "50 percent cap _ tokenized delegation _ delegation equals cap", +// globalLiquidCap: sdk.MustNewDecFromStr("0.5"), +// totalLiquidStake: sdk.NewInt(0), +// totalStake: sdk.NewInt(100), +// newLiquidStake: sdk.NewInt(50), +// tokenizingShares: true, +// expectedExceeds: false, +// }, +// { +// // Cap: 50% - Native Delegation - Delegation Below Threshold +// // Total Liquid Stake: 0, Total Stake: 100, New Liquid Stake: 51 +// // => Total Liquid Stake: 0+51=51, Total Stake: 100+51=151 => 51/151 = 33% < 50% cap +// name: "50 percent cap _ native delegation _ delegation below cap", +// globalLiquidCap: sdk.MustNewDecFromStr("0.5"), +// totalLiquidStake: sdk.NewInt(0), +// totalStake: sdk.NewInt(100), +// newLiquidStake: sdk.NewInt(51), +// tokenizingShares: false, +// expectedExceeds: false, +// }, +// { +// // Cap: 50% - Tokenized Delegation - Delegation Exceeds Threshold +// // Total Liquid Stake: 0, Total Stake: 100, New Liquid Stake: 51 +// // => 51 / 100 = 51% > 50% cap +// name: "50 percent cap _ tokenized delegation _delegation exceeds cap", +// globalLiquidCap: sdk.MustNewDecFromStr("0.5"), +// totalLiquidStake: sdk.NewInt(0), +// totalStake: sdk.NewInt(100), +// newLiquidStake: sdk.NewInt(51), +// tokenizingShares: true, +// expectedExceeds: true, +// }, +// { +// // Cap of 0% - everything should exceed +// name: "0 percent cap", +// globalLiquidCap: sdk.ZeroDec(), +// totalLiquidStake: sdk.NewInt(0), +// totalStake: sdk.NewInt(1_000_000), +// newLiquidStake: sdk.NewInt(1), +// tokenizingShares: false, +// expectedExceeds: true, +// }, +// { +// // Cap of 100% - nothing should exceed +// name: "100 percent cap", +// globalLiquidCap: sdk.OneDec(), +// totalLiquidStake: sdk.NewInt(1), +// totalStake: sdk.NewInt(1), +// newLiquidStake: sdk.NewInt(1_000_000), +// tokenizingShares: false, +// expectedExceeds: false, +// }, +// } + +// for _, tc := range testCases { +// t.Run(tc.name, func(t *testing.T) { +// // Update the global liquid staking cap +// params := keeper.GetParams(ctx) +// params.GlobalLiquidStakingCap = tc.globalLiquidCap +// keeper.SetParams(ctx, params) + +// // Update the total liquid tokens +// keeper.SetTotalLiquidStakedTokens(ctx, tc.totalLiquidStake) + +// // Fund each pool for the given test case +// clearPoolBalance(t, app, ctx) +// fundPoolBalance(t, app, ctx, tc.totalStake) + +// // Check if the new tokens would exceed the global cap +// actualExceeds := keeper.CheckExceedsGlobalLiquidStakingCap(ctx, tc.newLiquidStake, tc.tokenizingShares) +// require.Equal(t, tc.expectedExceeds, actualExceeds, tc.name) +// }) +// } +// } + +// // Tests SafelyIncreaseTotalLiquidStakedTokens +// func (s *KeeperTestSuite) TestSafelyIncreaseTotalLiquidStakedTokens(t *testing.T) { +// ctx, keeper := s.ctx, s.stakingKeeper +// require := s.Require() + +// intitialTotalLiquidStaked := sdk.NewInt(100) +// increaseAmount := sdk.NewInt(10) +// poolBalance := sdk.NewInt(200) + +// // Set the total staked and total liquid staked amounts +// // which are required components when checking the global cap +// // Total stake is calculated from the pool balance +// clearPoolBalance(t, app, ctx) +// fundPoolBalance(t, app, ctx, poolBalance) +// keeper.SetTotalLiquidStakedTokens(ctx, intitialTotalLiquidStaked) + +// // Set the global cap such that a small delegation would exceed the cap +// params := keeper.GetParams(ctx) +// params.GlobalLiquidStakingCap = sdk.MustNewDecFromStr("0.0001") +// keeper.SetParams(ctx, params) + +// // Attempt to increase the total liquid stake again, it should error since +// // the cap was exceeded +// err := keeper.SafelyIncreaseTotalLiquidStakedTokens(ctx, increaseAmount, true) +// require.ErrorIs(t, err, types.ErrGlobalLiquidStakingCapExceeded) +// require.Equal(t, intitialTotalLiquidStaked, keeper.GetTotalLiquidStakedTokens(ctx)) + +// // Now relax the cap so that the increase succeeds +// params.GlobalLiquidStakingCap = sdk.MustNewDecFromStr("0.99") +// keeper.SetParams(ctx, params) + +// // Confirm the total increased +// err = keeper.SafelyIncreaseTotalLiquidStakedTokens(ctx, increaseAmount, true) +// require.NoError(t, err) +// require.Equal(t, intitialTotalLiquidStaked.Add(increaseAmount), keeper.GetTotalLiquidStakedTokens(ctx)) +// } + +// // Tests DecreaseTotalLiquidStakedTokens +// func (s *KeeperTestSuite) TestDecreaseTotalLiquidStakedTokens(t *testing.T) { +// ctx, keeper := s.ctx, s.stakingKeeper +// require := s.Require() + +// intitialTotalLiquidStaked := sdk.NewInt(100) +// decreaseAmount := sdk.NewInt(10) + +// // Set the total liquid staked to an arbitrary value +// keeper.SetTotalLiquidStakedTokens(ctx, intitialTotalLiquidStaked) + +// // Decrease the total liquid stake and confirm the total was updated +// err := keeper.DecreaseTotalLiquidStakedTokens(ctx, decreaseAmount) +// require.NoError(t, err, "no error expected when decreasing total liquid staked tokens") +// require.Equal(t, intitialTotalLiquidStaked.Sub(decreaseAmount), keeper.GetTotalLiquidStakedTokens(ctx)) + +// // Attempt to decrease by an excessive amount, it should error +// err = keeper.DecreaseTotalLiquidStakedTokens(ctx, intitialTotalLiquidStaked) +// require.ErrorIs(err, types.ErrTotalLiquidStakedUnderflow) +// } + +// // Tests CheckExceedsValidatorBondCap +// func (s *KeeperTestSuite) TestCheckExceedsValidatorBondCap(t *testing.T) { +// ctx, keeper := s.ctx, s.stakingKeeper +// require := s.Require() + +// testCases := []struct { +// name string +// validatorShares sdk.Dec +// validatorBondFactor sdk.Dec +// currentLiquidShares sdk.Dec +// newShares sdk.Dec +// expectedExceeds bool +// }{ +// { +// // Validator Shares: 100, Factor: 1, Current Shares: 90 => 100 Max Shares, Capacity: 10 +// // New Shares: 5 - below cap +// name: "factor 1 - below cap", +// validatorShares: sdk.NewDec(100), +// validatorBondFactor: sdk.NewDec(1), +// currentLiquidShares: sdk.NewDec(90), +// newShares: sdk.NewDec(5), +// expectedExceeds: false, +// }, +// { +// // Validator Shares: 100, Factor: 1, Current Shares: 90 => 100 Max Shares, Capacity: 10 +// // New Shares: 10 - at cap +// name: "factor 1 - at cap", +// validatorShares: sdk.NewDec(100), +// validatorBondFactor: sdk.NewDec(1), +// currentLiquidShares: sdk.NewDec(90), +// newShares: sdk.NewDec(10), +// expectedExceeds: false, +// }, +// { +// // Validator Shares: 100, Factor: 1, Current Shares: 90 => 100 Max Shares, Capacity: 10 +// // New Shares: 15 - above cap +// name: "factor 1 - above cap", +// validatorShares: sdk.NewDec(100), +// validatorBondFactor: sdk.NewDec(1), +// currentLiquidShares: sdk.NewDec(90), +// newShares: sdk.NewDec(15), +// expectedExceeds: true, +// }, +// { +// // Validator Shares: 100, Factor: 2, Current Shares: 90 => 200 Max Shares, Capacity: 110 +// // New Shares: 5 - below cap +// name: "factor 2 - well below cap", +// validatorShares: sdk.NewDec(100), +// validatorBondFactor: sdk.NewDec(2), +// currentLiquidShares: sdk.NewDec(90), +// newShares: sdk.NewDec(5), +// expectedExceeds: false, +// }, +// { +// // Validator Shares: 100, Factor: 2, Current Shares: 90 => 200 Max Shares, Capacity: 110 +// // New Shares: 100 - below cap +// name: "factor 2 - below cap", +// validatorShares: sdk.NewDec(100), +// validatorBondFactor: sdk.NewDec(2), +// currentLiquidShares: sdk.NewDec(90), +// newShares: sdk.NewDec(100), +// expectedExceeds: false, +// }, +// { +// // Validator Shares: 100, Factor: 2, Current Shares: 90 => 200 Max Shares, Capacity: 110 +// // New Shares: 110 - below cap +// name: "factor 2 - at cap", +// validatorShares: sdk.NewDec(100), +// validatorBondFactor: sdk.NewDec(2), +// currentLiquidShares: sdk.NewDec(90), +// newShares: sdk.NewDec(110), +// expectedExceeds: false, +// }, +// { +// // Validator Shares: 100, Factor: 2, Current Shares: 90 => 200 Max Shares, Capacity: 110 +// // New Shares: 111 - above cap +// name: "factor 2 - above cap", +// validatorShares: sdk.NewDec(100), +// validatorBondFactor: sdk.NewDec(2), +// currentLiquidShares: sdk.NewDec(90), +// newShares: sdk.NewDec(111), +// expectedExceeds: true, +// }, +// { +// // Validator Shares: 100, Factor: 100, Current Shares: 90 => 10000 Max Shares, Capacity: 9910 +// // New Shares: 100 - below cap +// name: "factor 100 - below cap", +// validatorShares: sdk.NewDec(100), +// validatorBondFactor: sdk.NewDec(100), +// currentLiquidShares: sdk.NewDec(90), +// newShares: sdk.NewDec(100), +// expectedExceeds: false, +// }, +// { +// // Validator Shares: 100, Factor: 100, Current Shares: 90 => 10000 Max Shares, Capacity: 9910 +// // New Shares: 9910 - at cap +// name: "factor 100 - at cap", +// validatorShares: sdk.NewDec(100), +// validatorBondFactor: sdk.NewDec(100), +// currentLiquidShares: sdk.NewDec(90), +// newShares: sdk.NewDec(9910), +// expectedExceeds: false, +// }, +// { +// // Validator Shares: 100, Factor: 100, Current Shares: 90 => 10000 Max Shares, Capacity: 9910 +// // New Shares: 9911 - above cap +// name: "factor 100 - above cap", +// validatorShares: sdk.NewDec(100), +// validatorBondFactor: sdk.NewDec(100), +// currentLiquidShares: sdk.NewDec(90), +// newShares: sdk.NewDec(9911), +// expectedExceeds: true, +// }, +// { +// // Factor of -1 (disabled): Should always return false +// name: "factor disabled", +// validatorShares: sdk.NewDec(1), +// validatorBondFactor: sdk.NewDec(-1), +// currentLiquidShares: sdk.NewDec(1), +// newShares: sdk.NewDec(1_000_000), +// expectedExceeds: false, +// }, +// } + +// for _, tc := range testCases { +// t.Run(tc.name, func(t *testing.T) { +// // Update the validator bond factor +// params := keeper.GetParams(ctx) +// params.ValidatorBondFactor = tc.validatorBondFactor +// keeper.SetParams(ctx, params) + +// // Create a validator with designated self-bond shares +// validator := types.Validator{ +// LiquidShares: tc.currentLiquidShares, +// ValidatorBondShares: tc.validatorShares, +// } + +// // Check whether the cap is exceeded +// actualExceeds := keeper.CheckExceedsValidatorBondCap(ctx, validator, tc.newShares) +// require.Equal(t, tc.expectedExceeds, actualExceeds, tc.name) +// }) +// } +// } + +// // Tests TestCheckExceedsValidatorLiquidStakingCap +// func (s *KeeperTestSuite) TestCheckExceedsValidatorLiquidStakingCap(t *testing.T) { +// ctx, keeper := s.ctx, s.stakingKeeper +// require := s.Require() + +// testCases := []struct { +// name string +// validatorLiquidCap sdk.Dec +// validatorLiquidShares sdk.Dec +// validatorTotalShares sdk.Dec +// newLiquidShares sdk.Dec +// expectedExceeds bool +// }{ +// { +// // Cap: 10% - Delegation Below Threshold +// // Liquid Shares: 5, Total Shares: 95, New Liquid Shares: 1 +// // => Liquid Shares: 5+1=6, Total Shares: 95+1=96 => 6/96 = 6% < 10% cap +// name: "10 percent cap _ delegation below cap", +// validatorLiquidCap: sdk.MustNewDecFromStr("0.1"), +// validatorLiquidShares: sdk.NewDec(5), +// validatorTotalShares: sdk.NewDec(95), +// newLiquidShares: sdk.NewDec(1), +// expectedExceeds: false, +// }, +// { +// // Cap: 10% - Delegation At Threshold +// // Liquid Shares: 5, Total Shares: 95, New Liquid Shares: 5 +// // => Liquid Shares: 5+5=10, Total Shares: 95+5=100 => 10/100 = 10% == 10% cap +// name: "10 percent cap _ delegation equals cap", +// validatorLiquidCap: sdk.MustNewDecFromStr("0.1"), +// validatorLiquidShares: sdk.NewDec(5), +// validatorTotalShares: sdk.NewDec(95), +// newLiquidShares: sdk.NewDec(4), +// expectedExceeds: false, +// }, +// { +// // Cap: 10% - Delegation Exceeds Threshold +// // Liquid Shares: 5, Total Shares: 95, New Liquid Shares: 6 +// // => Liquid Shares: 5+6=11, Total Shares: 95+6=101 => 11/101 = 11% > 10% cap +// name: "10 percent cap _ delegation exceeds cap", +// validatorLiquidCap: sdk.MustNewDecFromStr("0.1"), +// validatorLiquidShares: sdk.NewDec(5), +// validatorTotalShares: sdk.NewDec(95), +// newLiquidShares: sdk.NewDec(6), +// expectedExceeds: true, +// }, +// { +// // Cap: 20% - Delegation Below Threshold +// // Liquid Shares: 20, Total Shares: 220, New Liquid Shares: 29 +// // => Liquid Shares: 20+29=49, Total Shares: 220+29=249 => 49/249 = 19% < 20% cap +// name: "20 percent cap _ delegation below cap", +// validatorLiquidCap: sdk.MustNewDecFromStr("0.2"), +// validatorLiquidShares: sdk.NewDec(20), +// validatorTotalShares: sdk.NewDec(220), +// newLiquidShares: sdk.NewDec(29), +// expectedExceeds: false, +// }, +// { +// // Cap: 20% - Delegation At Threshold +// // Liquid Shares: 20, Total Shares: 220, New Liquid Shares: 30 +// // => Liquid Shares: 20+30=50, Total Shares: 220+30=250 => 50/250 = 20% == 20% cap +// name: "20 percent cap _ delegation equals cap", +// validatorLiquidCap: sdk.MustNewDecFromStr("0.2"), +// validatorLiquidShares: sdk.NewDec(20), +// validatorTotalShares: sdk.NewDec(220), +// newLiquidShares: sdk.NewDec(30), +// expectedExceeds: false, +// }, +// { +// // Cap: 20% - Delegation Exceeds Threshold +// // Liquid Shares: 20, Total Shares: 220, New Liquid Shares: 31 +// // => Liquid Shares: 20+31=51, Total Shares: 220+31=251 => 51/251 = 21% > 20% cap +// name: "20 percent cap _ delegation exceeds cap", +// validatorLiquidCap: sdk.MustNewDecFromStr("0.2"), +// validatorLiquidShares: sdk.NewDec(20), +// validatorTotalShares: sdk.NewDec(220), +// newLiquidShares: sdk.NewDec(31), +// expectedExceeds: true, +// }, +// { +// // Cap of 0% - everything should exceed +// name: "0 percent cap", +// validatorLiquidCap: sdk.ZeroDec(), +// validatorLiquidShares: sdk.NewDec(0), +// validatorTotalShares: sdk.NewDec(1_000_000), +// newLiquidShares: sdk.NewDec(1), +// expectedExceeds: true, +// }, +// { +// // Cap of 100% - nothing should exceed +// name: "100 percent cap", +// validatorLiquidCap: sdk.OneDec(), +// validatorLiquidShares: sdk.NewDec(1), +// validatorTotalShares: sdk.NewDec(1_000_000), +// newLiquidShares: sdk.NewDec(1), +// expectedExceeds: false, +// }, +// } + +// for _, tc := range testCases { +// t.Run(tc.name, func(t *testing.T) { +// // Update the validator liquid staking cap +// params := keeper.GetParams(ctx) +// params.ValidatorLiquidStakingCap = tc.validatorLiquidCap +// keeper.SetParams(ctx, params) + +// // Create a validator with designated self-bond shares +// validator := types.Validator{ +// LiquidShares: tc.validatorLiquidShares, +// DelegatorShares: tc.validatorTotalShares, +// } + +// // Check whether the cap is exceeded +// actualExceeds := keeper.CheckExceedsValidatorLiquidStakingCap(ctx, validator, tc.newLiquidShares) +// require.Equal(t, tc.expectedExceeds, actualExceeds, tc.name) +// }) +// } +// } + +// // Tests SafelyIncreaseValidatorLiquidShares +// func (s *KeeperTestSuite) TestSafelyIncreaseValidatorLiquidShares(t *testing.T) { +// ctx, keeper := s.ctx, s.stakingKeeper +// require := s.Require() + +// // Generate a test validator address +// privKey := secp256k1.GenPrivKey() +// pubKey := privKey.PubKey() +// valAddress := sdk.ValAddress(pubKey.Address()) + +// // Helper function to check the validator's liquid shares +// checkValidatorLiquidShares := func(expected sdk.Dec, description string) { +// actualValidator, found := keeper.GetValidator(ctx, valAddress) +// require.True(found) +// require.Equal(expected.TruncateInt64(), actualValidator.LiquidShares.TruncateInt64(), description) +// } + +// // Start with the following: +// // Initial Liquid Shares: 0 +// // Validator Bond Shares: 10 +// // Validator TotalShares: 75 +// // +// // Initial Caps: +// // ValidatorBondFactor: 1 (Cap applied at 10 shares) +// // ValidatorLiquidStakingCap: 25% (Cap applied at 25 shares) +// // +// // Cap Increases: +// // ValidatorBondFactor: 10 (Cap applied at 100 shares) +// // ValidatorLiquidStakingCap: 40% (Cap applied at 50 shares) +// initialLiquidShares := sdk.NewDec(0) +// validatorBondShares := sdk.NewDec(10) +// validatorTotalShares := sdk.NewDec(75) + +// firstIncreaseAmount := sdk.NewDec(20) +// secondIncreaseAmount := sdk.NewDec(10) // total increase of 30 + +// initialBondFactor := sdk.NewDec(1) +// finalBondFactor := sdk.NewDec(10) +// initialLiquidStakingCap := sdk.MustNewDecFromStr("0.25") +// finalLiquidStakingCap := sdk.MustNewDecFromStr("0.4") + +// // Create a validator with designated self-bond shares +// initialValidator := types.Validator{ +// OperatorAddress: valAddress.String(), +// LiquidShares: initialLiquidShares, +// ValidatorBondShares: validatorBondShares, +// DelegatorShares: validatorTotalShares, +// } +// keeper.SetValidator(ctx, initialValidator) + +// // Set validator bond factor to a small number such that any delegation would fail, +// // and set the liquid staking cap such that the first stake would succeed, but the second +// // would fail +// params := keeper.GetParams(ctx) +// params.ValidatorBondFactor = initialBondFactor +// params.ValidatorLiquidStakingCap = initialLiquidStakingCap +// keeper.SetParams(ctx, params) + +// // Attempt to increase the validator liquid shares, it should throw an +// // error that the validator bond cap was exceeded +// _, err := keeper.SafelyIncreaseValidatorLiquidShares(ctx, valAddress, firstIncreaseAmount) +// require.ErrorIs(t, err, types.ErrInsufficientValidatorBondShares) +// checkValidatorLiquidShares(initialLiquidShares, "shares after low bond factor") + +// // Change validator bond factor to a more conservative number, so that the increase succeeds +// params.ValidatorBondFactor = finalBondFactor +// keeper.SetParams(ctx, params) + +// // Try the increase again and check that it succeeded +// expectedLiquidSharesAfterFirstStake := initialLiquidShares.Add(firstIncreaseAmount) +// _, err = keeper.SafelyIncreaseValidatorLiquidShares(ctx, valAddress, firstIncreaseAmount) +// require.NoError(t, err) +// checkValidatorLiquidShares(expectedLiquidSharesAfterFirstStake, "shares with cap loose bond cap") + +// // Attempt another increase, it should fail from the liquid staking cap +// _, err = keeper.SafelyIncreaseValidatorLiquidShares(ctx, valAddress, secondIncreaseAmount) +// require.ErrorIs(t, err, types.ErrValidatorLiquidStakingCapExceeded) +// checkValidatorLiquidShares(expectedLiquidSharesAfterFirstStake, "shares after liquid staking cap hit") + +// // Raise the liquid staking cap so the new increment succeeds +// params.ValidatorLiquidStakingCap = finalLiquidStakingCap +// keeper.SetParams(ctx, params) + +// // Finally confirm that the increase succeeded this time +// expectedLiquidSharesAfterSecondStake := expectedLiquidSharesAfterFirstStake.Add(secondIncreaseAmount) +// _, err = keeper.SafelyIncreaseValidatorLiquidShares(ctx, valAddress, secondIncreaseAmount) +// require.NoError(t, err, "no error expected after increasing liquid staking cap") +// checkValidatorLiquidShares(expectedLiquidSharesAfterSecondStake, "shares after loose liquid stake cap") +// } + +// // Tests DecreaseValidatorLiquidShares +// func (s *KeeperTestSuite) TestDecreaseValidatorLiquidShares(t *testing.T) { +// ctx, keeper := s.ctx, s.stakingKeeper +// require := s.Require() + +// initialLiquidShares := sdk.NewDec(100) +// decreaseAmount := sdk.NewDec(10) + +// // Create a validator with designated self-bond shares +// privKey := secp256k1.GenPrivKey() +// pubKey := privKey.PubKey() +// valAddress := sdk.ValAddress(pubKey.Address()) + +// initialValidator := types.Validator{ +// OperatorAddress: valAddress.String(), +// LiquidShares: initialLiquidShares, +// } +// keeper.SetValidator(ctx, initialValidator) + +// // Decrease the validator liquid shares, and confirm the new share amount has been updated +// _, err := keeper.DecreaseValidatorLiquidShares(ctx, valAddress, decreaseAmount) +// require.NoError(t, err, "no error expected when decreasing validator liquid shares") + +// actualValidator, found := keeper.GetValidator(ctx, valAddress) +// require.True(t, found) +// require.Equal(t, initialLiquidShares.Sub(decreaseAmount), actualValidator.LiquidShares, "liquid shares") + +// // Attempt to decrease by a larger amount than it has, it should fail +// _, err = keeper.DecreaseValidatorLiquidShares(ctx, valAddress, initialLiquidShares) +// require.ErrorIs(t, err, types.ErrValidatorLiquidSharesUnderflow) +// } + +// // Tests SafelyDecreaseValidatorBond +// func (s *KeeperTestSuite) TestSafelyDecreaseValidatorBond(t *testing.T) { +// ctx, keeper := s.ctx, s.stakingKeeper +// require := s.Require() + +// // Initial Bond Factor: 100, Initial Validator Bond: 10 +// // => Max Liquid Shares 1000 (Initial Liquid Shares: 200) +// initialBondFactor := sdk.NewDec(100) +// initialValidatorBondShares := sdk.NewDec(10) +// initialLiquidShares := sdk.NewDec(200) + +// // Create a validator with designated self-bond shares +// privKey := secp256k1.GenPrivKey() +// pubKey := privKey.PubKey() +// valAddress := sdk.ValAddress(pubKey.Address()) + +// initialValidator := types.Validator{ +// OperatorAddress: valAddress.String(), +// ValidatorBondShares: initialValidatorBondShares, +// LiquidShares: initialLiquidShares, +// } +// keeper.SetValidator(ctx, initialValidator) + +// // Set the bond factor +// params := keeper.GetParams(ctx) +// params.ValidatorBondFactor = initialBondFactor +// keeper.SetParams(ctx, params) + +// // Decrease the validator bond from 10 to 5 (minus 5) +// // This will adjust the cap (factor * shares) +// // from (100 * 10 = 1000) to (100 * 5 = 500) +// // Since this is still above the initial liquid shares of 200, this will succeed +// decreaseAmount, expectedBondShares := sdk.NewDec(5), sdk.NewDec(5) +// err := keeper.SafelyDecreaseValidatorBond(ctx, valAddress, decreaseAmount) +// require.NoError(t, err) + +// actualValidator, found := keeper.GetValidator(ctx, valAddress) +// require.True(t, found) +// require.Equal(t, expectedBondShares, actualValidator.ValidatorBondShares, "validator bond shares shares") + +// // Now attempt to decrease the validator bond again from 5 to 1 (minus 4) +// // This time, the cap will be reduced to (factor * shares) = (100 * 1) = 100 +// // However, the liquid shares are currently 200, so this should fail +// decreaseAmount, expectedBondShares = sdk.NewDec(4), sdk.NewDec(1) +// err = keeper.SafelyDecreaseValidatorBond(ctx, valAddress, decreaseAmount) +// require.ErrorIs(t, err, types.ErrInsufficientValidatorBondShares) + +// // Finally, disable the cap and attempt to decrease again +// // This time it should succeed +// params.ValidatorBondFactor = types.ValidatorBondCapDisabled +// keeper.SetParams(ctx, params) + +// err = keeper.SafelyDecreaseValidatorBond(ctx, valAddress, decreaseAmount) +// require.NoError(t, err) + +// actualValidator, found = keeper.GetValidator(ctx, valAddress) +// require.True(t, found) +// require.Equal(t, expectedBondShares, actualValidator.ValidatorBondShares, "validator bond shares shares") +// } + +// // Tests Add/Remove/Get/SetTokenizeSharesLock +// func (s *KeeperTestSuite) TestTokenizeSharesLock(t *testing.T) { +// ctx, keeper := s.ctx, s.stakingKeeper +// require := s.Require() + +// addresses := simtestutil.AddTestAddrs(s.bankKeeper, ctx, 2, sdk.NewInt(1)) +// addressA, addressB := addresses[0], addresses[1] + +// unlocked := types.TOKENIZE_SHARE_LOCK_STATUS_UNLOCKED.String() +// locked := types.TOKENIZE_SHARE_LOCK_STATUS_LOCKED.String() +// lockExpiring := types.TOKENIZE_SHARE_LOCK_STATUS_LOCK_EXPIRING.String() + +// // Confirm both accounts start unlocked +// status, _ := keeper.GetTokenizeSharesLock(ctx, addressA) +// require.Equal(t, unlocked, status.String(), "addressA unlocked at start") + +// status, _ = keeper.GetTokenizeSharesLock(ctx, addressB) +// require.Equal(t, unlocked, status.String(), "addressB unlocked at start") + +// // Lock the first account +// keeper.AddTokenizeSharesLock(ctx, addressA) + +// // The first account should now have tokenize shares disabled +// // and the unlock time should be the zero time +// status, _ = keeper.GetTokenizeSharesLock(ctx, addressA) +// require.Equal(t, locked, status.String(), "addressA locked") + +// status, _ = keeper.GetTokenizeSharesLock(ctx, addressB) +// require.Equal(t, unlocked, status.String(), "addressB still unlocked") + +// // Update the lock time and confirm it was set +// expectedUnlockTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) +// keeper.SetTokenizeSharesUnlockTime(ctx, addressA, expectedUnlockTime) + +// status, actualUnlockTime := keeper.GetTokenizeSharesLock(ctx, addressA) +// require.Equal(t, lockExpiring, status.String(), "addressA lock expiring") +// require.Equal(t, expectedUnlockTime, actualUnlockTime, "addressA unlock time") + +// // Confirm B is still unlocked +// status, _ = keeper.GetTokenizeSharesLock(ctx, addressB) +// require.Equal(t, unlocked, status.String(), "addressB still unlocked") + +// // Remove the lock +// keeper.RemoveTokenizeSharesLock(ctx, addressA) +// status, _ = keeper.GetTokenizeSharesLock(ctx, addressA) +// require.Equal(t, unlocked, status.String(), "addressA unlocked at end") + +// status, _ = keeper.GetTokenizeSharesLock(ctx, addressB) +// require.Equal(t, unlocked, status.String(), "addressB unlocked at end") +// } + +// // Tests GetAllTokenizeSharesLocks +// func (s *KeeperTestSuite) TestGetAllTokenizeSharesLocks(t *testing.T) { +// ctx, keeper := s.ctx, s.stakingKeeper +// require := s.Require() + +// addresses := simapp.AddTestAddrs(app, ctx, 4, sdk.NewInt(1)) + +// // Set 2 locked accounts, and two accounts with a lock expiring +// keeper.AddTokenizeSharesLock(ctx, addresses[0]) +// keeper.AddTokenizeSharesLock(ctx, addresses[1]) + +// unlockTime1 := time.Date(2023, 1, 1, 1, 0, 0, 0, time.UTC) +// unlockTime2 := time.Date(2023, 1, 2, 1, 0, 0, 0, time.UTC) +// keeper.SetTokenizeSharesUnlockTime(ctx, addresses[2], unlockTime1) +// keeper.SetTokenizeSharesUnlockTime(ctx, addresses[3], unlockTime2) + +// // Defined expected locks after GetAll +// expectedLocks := map[string]types.TokenizeShareLock{ +// addresses[0].String(): { +// Status: types.TOKENIZE_SHARE_LOCK_STATUS_LOCKED.String(), +// }, +// addresses[1].String(): { +// Status: types.TOKENIZE_SHARE_LOCK_STATUS_LOCKED.String(), +// }, +// addresses[2].String(): { +// Status: types.TOKENIZE_SHARE_LOCK_STATUS_LOCK_EXPIRING.String(), +// CompletionTime: unlockTime1, +// }, +// addresses[3].String(): { +// Status: types.TOKENIZE_SHARE_LOCK_STATUS_LOCK_EXPIRING.String(), +// CompletionTime: unlockTime2, +// }, +// } + +// // Check output from GetAll +// actualLocks := keeper.GetAllTokenizeSharesLocks(ctx) +// require.Len(actualLocks, len(expectedLocks), "number of locks") + +// for i, actual := range actualLocks { +// expected, ok := expectedLocks[actual.Address] +// require.True(ok, "address %s not expected", actual.Address) +// require.Equal(expected.Status, actual.Status, "tokenize share lock #%d status", i) +// require.Equal(expected.CompletionTime, actual.CompletionTime, "tokenize share lock #%d completion time", i) +// } +// } + +// // Test Get/SetPendingTokenizeShareAuthorizations +// func (s *KeeperTestSuite) TestPendingTokenizeShareAuthorizations(t *testing.T) { +// ctx, keeper := s.ctx, s.stakingKeeper +// require := s.Require() + +// // Create dummy accounts and completion times +// addresses := simapp.AddTestAddrs(app, ctx, 3, sdk.NewInt(1)) +// addressStrings := []string{} +// for _, address := range addresses { +// addressStrings = append(addressStrings, address.String()) +// } + +// timeA := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) +// timeB := timeA.Add(time.Hour) + +// // There should be no addresses returned originally +// authorizationsA := keeper.GetPendingTokenizeShareAuthorizations(ctx, timeA) +// require.Empty(t, authorizationsA.Addresses, "no addresses at timeA expected") + +// authorizationsB := keeper.GetPendingTokenizeShareAuthorizations(ctx, timeB) +// require.Empty(t, authorizationsB.Addresses, "no addresses at timeB expected") + +// // Store addresses for timeB +// keeper.SetPendingTokenizeShareAuthorizations(ctx, timeB, types.PendingTokenizeShareAuthorizations{ +// Addresses: addressStrings, +// }) + +// // Check addresses +// authorizationsA = keeper.GetPendingTokenizeShareAuthorizations(ctx, timeA) +// require.Empty(t, authorizationsA.Addresses, "no addresses at timeA expected at end") + +// authorizationsB = keeper.GetPendingTokenizeShareAuthorizations(ctx, timeB) +// require.Equal(t, addressStrings, authorizationsB.Addresses, "address length") +// } + +// // Test QueueTokenizeSharesAuthorization and RemoveExpiredTokenizeShareLocks +// func (s *KeeperTestSuite) TestTokenizeShareAuthorizationQueue(t *testing.T) { +// ctx, keeper := s.ctx, s.stakingKeeper +// require := s.Require() + +// // We'll start by adding the following addresses to the queue +// // Time 0: [address0] +// // Time 1: [] +// // Time 2: [address1, address2, address3] +// // Time 3: [address4, address5] +// // Time 4: [address6] +// addresses := simapp.AddTestAddrs(app, ctx, 7, sdk.NewInt(1)) +// addressesByTime := map[int][]sdk.AccAddress{ +// 0: {addresses[0]}, +// 1: {}, +// 2: {addresses[1], addresses[2], addresses[3]}, +// 3: {addresses[4], addresses[5]}, +// 4: {addresses[6]}, +// } + +// // Set the unbonding time to 1 day +// unbondingPeriod := time.Hour * 24 +// params := keeper.GetParams(ctx) +// params.UnbondingTime = unbondingPeriod +// keeper.SetParams(ctx, params) + +// // Add each address to the queue and then increment the block time +// // such that the times line up as follows +// // Time 0: 2023-01-01 00:00:00 +// // Time 1: 2023-01-01 00:01:00 +// // Time 2: 2023-01-01 00:02:00 +// // Time 3: 2023-01-01 00:03:00 +// startTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) +// ctx = ctx.WithBlockTime(startTime) +// blockTimeIncrement := time.Hour + +// for timeIndex := 0; timeIndex <= 4; timeIndex++ { +// for _, address := range addressesByTime[timeIndex] { +// keeper.QueueTokenizeSharesAuthorization(ctx, address) +// } +// ctx = ctx.WithBlockTime(ctx.BlockTime().Add(blockTimeIncrement)) +// } + +// // We'll unlock the tokens using the following progression +// // The "alias'"/keys for these times assume a starting point of the Time 0 +// // from above, plus the Unbonding Time +// // Time -1 (2023-01-01 23:59:99): [] +// // Time 0 (2023-01-02 00:00:00): [address0] +// // Time 1 (2023-01-02 00:01:00): [] +// // Time 2.5 (2023-01-02 00:02:30): [address1, address2, address3] +// // Time 10 (2023-01-02 00:10:00): [address4, address5, address6] +// unlockBlockTimes := map[string]time.Time{ +// "-1": startTime.Add(unbondingPeriod).Add(-time.Second), +// "0": startTime.Add(unbondingPeriod), +// "1": startTime.Add(unbondingPeriod).Add(blockTimeIncrement), +// "2.5": startTime.Add(unbondingPeriod).Add(2 * blockTimeIncrement).Add(blockTimeIncrement / 2), +// "10": startTime.Add(unbondingPeriod).Add(10 * blockTimeIncrement), +// } +// expectedUnlockedAddresses := map[string][]string{ +// "-1": {}, +// "0": {addresses[0].String()}, +// "1": {}, +// "2.5": {addresses[1].String(), addresses[2].String(), addresses[3].String()}, +// "10": {addresses[4].String(), addresses[5].String(), addresses[6].String()}, +// } + +// // Now we'll remove items from the queue sequentially +// // First check with a block time before the first expiration - it should remove no addresses +// actualAddresses := keeper.RemoveExpiredTokenizeShareLocks(ctx, unlockBlockTimes["-1"]) +// require.Equal(t, expectedUnlockedAddresses["-1"], actualAddresses, "no addresses unlocked from time -1") + +// // Then pass in (time 0 + unbonding time) - it should remove the first address +// actualAddresses = keeper.RemoveExpiredTokenizeShareLocks(ctx, unlockBlockTimes["0"]) +// require.Equal(t, expectedUnlockedAddresses["0"], actualAddresses, "one address unlocked from time 0") + +// // Now pass in (time 1 + unbonding time) - it should remove no addresses since +// // the address at time 0 was already removed +// actualAddresses = keeper.RemoveExpiredTokenizeShareLocks(ctx, unlockBlockTimes["1"]) +// require.Equal(t, expectedUnlockedAddresses["1"], actualAddresses, "no addresses unlocked from time 1") + +// // Now pass in (time 2.5 + unbonding time) - it should remove the three addresses from time 2 +// actualAddresses = keeper.RemoveExpiredTokenizeShareLocks(ctx, unlockBlockTimes["2.5"]) +// require.Equal(t, expectedUnlockedAddresses["2.5"], actualAddresses, "addresses unlocked from time 2.5") + +// // Finally pass in a block time far in the future, which should remove all the remaining locks +// actualAddresses = keeper.RemoveExpiredTokenizeShareLocks(ctx, unlockBlockTimes["10"]) +// require.Equal(t, expectedUnlockedAddresses["10"], actualAddresses, "addresses unlocked from time 10") +// } + +// // Test RefreshTotalLiquidStaked +// func (s *KeeperTestSuite) TestRefreshTotalLiquidStaked(t *testing.T) { +// ctx, keeper := s.ctx, s.stakingKeeper +// require := s.Require() + +// // Set an arbitrary total liquid staked tokens amount that will get overwritten by the refresh +// keeper.SetTotalLiquidStakedTokens(ctx, sdk.NewInt(999)) + +// // Add validator's with various exchange rates +// validators := []types.Validator{ +// { +// // Exchange rate of 1 +// OperatorAddress: "valA", +// Tokens: sdk.NewInt(100), +// DelegatorShares: sdk.NewDec(100), +// LiquidShares: sdk.NewDec(100), // should be overwritten +// }, +// { +// // Exchange rate of 0.9 +// OperatorAddress: "valB", +// Tokens: sdk.NewInt(90), +// DelegatorShares: sdk.NewDec(100), +// LiquidShares: sdk.NewDec(200), // should be overwritten +// }, +// { +// // Exchange rate of 0.75 +// OperatorAddress: "valC", +// Tokens: sdk.NewInt(75), +// DelegatorShares: sdk.NewDec(100), +// LiquidShares: sdk.NewDec(300), // should be overwritten +// }, +// } + +// // Add various delegations across the above validator's +// // Total Liquid Staked: 1,849 + 922 = 2,771 +// // Liquid Shares: +// // ValA: 400 + 325 = 725 +// // ValB: 860 + 580 = 1,440 +// // ValC: 900 + 100 = 1,000 +// expectedTotalLiquidStaked := int64(2771) +// expectedValidatorLiquidShares := map[string]sdk.Dec{ +// "valA": sdk.NewDec(725), +// "valB": sdk.NewDec(1440), +// "valC": sdk.NewDec(1000), +// } + +// delegations := []struct { +// delegation types.Delegation +// isLSTP bool +// isTokenized bool +// }{ +// // Delegator A - Not a liquid staking provider +// // Number of tokens/shares is irrelevant for this test +// { +// isLSTP: false, +// delegation: types.Delegation{ +// DelegatorAddress: "delA", +// ValidatorAddress: "valA", +// Shares: sdk.NewDec(100), +// }, +// }, +// { +// isLSTP: false, +// delegation: types.Delegation{ +// DelegatorAddress: "delA", +// ValidatorAddress: "valB", +// Shares: sdk.NewDec(860), +// }, +// }, +// { +// isLSTP: false, +// delegation: types.Delegation{ +// DelegatorAddress: "delA", +// ValidatorAddress: "valC", +// Shares: sdk.NewDec(750), +// }, +// }, +// // Delegator B - Liquid staking provider, tokens included in total +// // Total liquid staked: 400 + 774 + 675 = 1,849 +// { +// // Shares: 400 shares, Exchange Rate: 1.0, Tokens: 400 +// isLSTP: true, +// delegation: types.Delegation{ +// DelegatorAddress: "delB-LSTP", +// ValidatorAddress: "valA", +// Shares: sdk.NewDec(400), +// }, +// }, +// { +// // Shares: 860 shares, Exchange Rate: 0.9, Tokens: 774 +// isLSTP: true, +// delegation: types.Delegation{ +// DelegatorAddress: "delB-LSTP", +// ValidatorAddress: "valB", +// Shares: sdk.NewDec(860), +// }, +// }, +// { +// // Shares: 900 shares, Exchange Rate: 0.75, Tokens: 675 +// isLSTP: true, +// delegation: types.Delegation{ +// DelegatorAddress: "delB-LSTP", +// ValidatorAddress: "valC", +// Shares: sdk.NewDec(900), +// }, +// }, +// // Delegator C - Tokenized shares, tokens included in total +// // Total liquid staked: 325 + 522 + 75 = 922 +// { +// // Shares: 325 shares, Exchange Rate: 1.0, Tokens: 325 +// isTokenized: true, +// delegation: types.Delegation{ +// DelegatorAddress: "delC-LSTP", +// ValidatorAddress: "valA", +// Shares: sdk.NewDec(325), +// }, +// }, +// { +// // Shares: 580 shares, Exchange Rate: 0.9, Tokens: 522 +// isTokenized: true, +// delegation: types.Delegation{ +// DelegatorAddress: "delC-LSTP", +// ValidatorAddress: "valB", +// Shares: sdk.NewDec(580), +// }, +// }, +// { +// // Shares: 100 shares, Exchange Rate: 0.75, Tokens: 75 +// isTokenized: true, +// delegation: types.Delegation{ +// DelegatorAddress: "delC-LSTP", +// ValidatorAddress: "valC", +// Shares: sdk.NewDec(100), +// }, +// }, +// } + +// // Create validators based on the above (must use an actual validator address) +// addresses := testutil.AddTestAddrsIncremental(s.bankKeeper, ctx, 5, keeper.TokensFromConsensusPower(ctx, 300)) +// validatorAddresses := map[string]sdk.ValAddress{ +// "valA": sdk.ValAddress(addresses[0]), +// "valB": sdk.ValAddress(addresses[1]), +// "valC": sdk.ValAddress(addresses[2]), +// } +// for _, validator := range validators { +// validator.OperatorAddress = validatorAddresses[validator.OperatorAddress].String() +// keeper.SetValidator(ctx, validator) +// } + +// // Create the delegations based on the above (must use actual delegator addresses) +// for _, delegationCase := range delegations { +// var delegatorAddress sdk.AccAddress +// switch { +// case delegationCase.isLSTP: +// delegatorAddress = createICAAccount(app, ctx) +// case delegationCase.isTokenized: +// delegatorAddress = createTokenizeShareModuleAccount(1) +// default: +// delegatorAddress = createBaseAccount(app, ctx, delegationCase.delegation.DelegatorAddress) +// } + +// delegation := delegationCase.delegation +// delegation.DelegatorAddress = delegatorAddress.String() +// delegation.ValidatorAddress = validatorAddresses[delegation.ValidatorAddress].String() +// keeper.SetDelegation(ctx, delegation) +// } + +// // Refresh the total liquid staked and validator liquid shares +// err := keeper.RefreshTotalLiquidStaked(ctx) +// require.NoError(t, err, "no error expected when refreshing total liquid staked") + +// // Check the total liquid staked and liquid shares by validator +// actualTotalLiquidStaked := keeper.GetTotalLiquidStakedTokens(ctx) +// require.Equal(t, expectedTotalLiquidStaked, actualTotalLiquidStaked.Int64(), "total liquid staked tokens") + +// for _, moniker := range []string{"valA", "valB", "valC"} { +// address := validatorAddresses[moniker] +// expectedLiquidShares := expectedValidatorLiquidShares[moniker] + +// actualValidator, found := keeper.GetValidator(ctx, address) +// require.True(t, found, "validator %s should have been found after refresh", moniker) + +// actualLiquidShares := actualValidator.LiquidShares +// require.Equal(t, expectedLiquidShares.TruncateInt64(), actualLiquidShares.TruncateInt64(), +// "liquid staked shares for validator %s", moniker) +// } +// } diff --git a/x/staking/keeper/msg_server.go b/x/staking/keeper/msg_server.go index 93615bcb137d..b9aa4b7ffddd 100644 --- a/x/staking/keeper/msg_server.go +++ b/x/staking/keeper/msg_server.go @@ -2,10 +2,12 @@ package keeper import ( "context" + "fmt" "strconv" "time" "github.com/armon/go-metrics" + vesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/exported" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -208,18 +210,47 @@ func (k msgServer) Delegate(goCtx context.Context, msg *types.MsgDelegate) (*typ ) } + tokens := msg.Amount.Amount + + // if this delegation is from a liquid staking provider (identified if the delegator + // is an ICA account), it cannot exceed the global or validator bond cap + if k.DelegatorIsLiquidStaker(delegatorAddress) { + shares, err := validator.SharesFromTokens(tokens) + if err != nil { + return nil, err + } + if err := k.SafelyIncreaseTotalLiquidStakedTokens(ctx, tokens, false); err != nil { + return nil, err + } + validator, err = k.SafelyIncreaseValidatorLiquidShares(ctx, valAddr, shares) + if err != nil { + return nil, err + } + } + // NOTE: source funds are always unbonded - newShares, err := k.Keeper.Delegate(ctx, delegatorAddress, msg.Amount.Amount, types.Unbonded, validator, true) + newShares, err := k.Keeper.Delegate(ctx, delegatorAddress, tokens, types.Unbonded, validator, true) if err != nil { return nil, err } - if msg.Amount.Amount.IsInt64() { + // If the delegation is a validator bond, increment the validator bond shares + delegation, found := k.Keeper.GetDelegation(ctx, delegatorAddress, valAddr) + if !found { + return nil, types.ErrNoDelegation + } + if delegation.ValidatorBond { + if err := k.IncreaseValidatorBondShares(ctx, valAddr, newShares); err != nil { + return nil, err + } + } + + if tokens.IsInt64() { defer func() { telemetry.IncrCounter(1, types.ModuleName, "delegate") telemetry.SetGaugeWithLabels( []string{"tx", "msg", msg.Type()}, - float32(msg.Amount.Amount.Int64()), + float32(tokens.Int64()), []metrics.Label{telemetry.NewLabel("denom", msg.Amount.Denom)}, ) }() @@ -241,21 +272,70 @@ func (k msgServer) Delegate(goCtx context.Context, msg *types.MsgDelegate) (*typ // BeginRedelegate defines a method for performing a redelegation of coins from a delegator and source validator to a destination validator func (k msgServer) BeginRedelegate(goCtx context.Context, msg *types.MsgBeginRedelegate) (*types.MsgBeginRedelegateResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) + valSrcAddr, err := sdk.ValAddressFromBech32(msg.ValidatorSrcAddress) if err != nil { return nil, err } + valDstAddr, err := sdk.ValAddressFromBech32(msg.ValidatorDstAddress) + if err != nil { + return nil, err + } + + _, found := k.GetValidator(ctx, valSrcAddr) + if !found { + return nil, types.ErrNoValidatorFound + } + dstValidator, found := k.GetValidator(ctx, valDstAddr) + if !found { + return nil, types.ErrNoValidatorFound + } + delegatorAddress, err := sdk.AccAddressFromBech32(msg.DelegatorAddress) if err != nil { return nil, err } - shares, err := k.ValidateUnbondAmount( + + srcDelegation, found := k.GetDelegation(ctx, delegatorAddress, valSrcAddr) + if !found { + return nil, status.Errorf( + codes.NotFound, + "delegation with delegator %s not found for validator %s", + msg.DelegatorAddress, msg.ValidatorSrcAddress, + ) + } + + srcShares, err := k.ValidateUnbondAmount( ctx, delegatorAddress, valSrcAddr, msg.Amount.Amount, ) if err != nil { return nil, err } + // If this is a validator self-bond, the new liquid delegation cannot fall below the self-bond * bond factor + // The delegation on the new validator will not a validator bond + if srcDelegation.ValidatorBond { + if err := k.SafelyDecreaseValidatorBond(ctx, valSrcAddr, srcShares); err != nil { + return nil, err + } + } + + // If this delegation from a liquid staker, the delegation on the new validator + // cannot exceed that validator's self-bond cap + // The liquid shares from the source validator should get moved to the destination validator + if k.DelegatorIsLiquidStaker(delegatorAddress) { + dstShares, err := dstValidator.SharesFromTokensTruncated(msg.Amount.Amount) + if err != nil { + return nil, err + } + if _, err := k.SafelyIncreaseValidatorLiquidShares(ctx, valDstAddr, dstShares); err != nil { + return nil, err + } + if _, err := k.DecreaseValidatorLiquidShares(ctx, valSrcAddr, srcShares); err != nil { + return nil, err + } + } + bondDenom := k.BondDenom(ctx) if msg.Amount.Denom != bondDenom { return nil, sdkerrors.Wrapf( @@ -263,18 +343,28 @@ func (k msgServer) BeginRedelegate(goCtx context.Context, msg *types.MsgBeginRed ) } - valDstAddr, err := sdk.ValAddressFromBech32(msg.ValidatorDstAddress) - if err != nil { - return nil, err - } - completionTime, err := k.BeginRedelegation( - ctx, delegatorAddress, valSrcAddr, valDstAddr, shares, + ctx, delegatorAddress, valSrcAddr, valDstAddr, srcShares, ) if err != nil { return nil, err } + // If the redelegation adds to a validator bond delegation, update the validator's bond shares + dstDelegation, found := k.GetDelegation(ctx, delegatorAddress, valDstAddr) + if !found { + return nil, types.ErrNoDelegation + } + if dstDelegation.ValidatorBond { + dstShares, err := dstValidator.SharesFromTokensTruncated(msg.Amount.Amount) + if err != nil { + return nil, err + } + if err := k.IncreaseValidatorBondShares(ctx, valDstAddr, dstShares); err != nil { + return nil, err + } + } + if msg.Amount.Amount.IsInt64() { defer func() { telemetry.IncrCounter(1, types.ModuleName, "redelegate") @@ -313,13 +403,47 @@ func (k msgServer) Undelegate(goCtx context.Context, msg *types.MsgUndelegate) ( if err != nil { return nil, err } + + tokens := msg.Amount.Amount shares, err := k.ValidateUnbondAmount( - ctx, delegatorAddress, addr, msg.Amount.Amount, + ctx, delegatorAddress, addr, tokens, ) if err != nil { return nil, err } + _, found := k.GetValidator(ctx, addr) + if !found { + return nil, types.ErrNoValidatorFound + } + + delegation, found := k.GetDelegation(ctx, delegatorAddress, addr) + if !found { + return nil, status.Errorf( + codes.NotFound, + "delegation with delegator %s not found for validator %s", + msg.DelegatorAddress, msg.ValidatorAddress, + ) + } + + // if this is a validator self-bond, the new liquid delegation cannot fall below the self-bond * bond factor + if delegation.ValidatorBond { + if err := k.SafelyDecreaseValidatorBond(ctx, addr, shares); err != nil { + return nil, err + } + } + + // if this delegation is from a liquid staking provider (identified if the delegator + // is an ICA account), the global and validator liquid totals should be decremented + if k.DelegatorIsLiquidStaker(delegatorAddress) { + if err := k.DecreaseTotalLiquidStakedTokens(ctx, tokens); err != nil { + return nil, err + } + if _, err := k.DecreaseValidatorLiquidShares(ctx, addr, shares); err != nil { + return nil, err + } + } + bondDenom := k.BondDenom(ctx) if msg.Amount.Denom != bondDenom { return nil, sdkerrors.Wrapf( @@ -332,12 +456,12 @@ func (k msgServer) Undelegate(goCtx context.Context, msg *types.MsgUndelegate) ( return nil, err } - if msg.Amount.Amount.IsInt64() { + if tokens.IsInt64() { defer func() { telemetry.IncrCounter(1, types.ModuleName, "undelegate") telemetry.SetGaugeWithLabels( []string{"tx", "msg", msg.Type()}, - float32(msg.Amount.Amount.Int64()), + float32(tokens.Int64()), []metrics.Label{telemetry.NewLabel("denom", msg.Amount.Denom)}, ) }() @@ -358,8 +482,6 @@ func (k msgServer) Undelegate(goCtx context.Context, msg *types.MsgUndelegate) ( }, nil } -// CancelUnbondingDelegation defines a method for canceling the unbonding delegation -// and delegate back to the validator. func (k msgServer) CancelUnbondingDelegation(goCtx context.Context, msg *types.MsgCancelUnbondingDelegation) (*types.MsgCancelUnbondingDelegationResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) @@ -405,6 +527,23 @@ func (k msgServer) CancelUnbondingDelegation(goCtx context.Context, msg *types.M ) } + // if this undelegation was from a liquid staking provider (identified if the delegator + // is an ICA account), the global and validator liquid totals should be incremented + tokens := msg.Amount.Amount + if k.DelegatorIsLiquidStaker(delegatorAddress) { + shares, err := validator.SharesFromTokens(tokens) + if err != nil { + return nil, err + } + if err := k.SafelyIncreaseTotalLiquidStakedTokens(ctx, tokens, false); err != nil { + return nil, err + } + validator, err = k.SafelyIncreaseValidatorLiquidShares(ctx, valAddr, shares) + if err != nil { + return nil, err + } + } + var ( unbondEntry types.UnbondingDelegationEntry unbondEntryIndex int64 = -1 @@ -430,11 +569,22 @@ func (k msgServer) CancelUnbondingDelegation(goCtx context.Context, msg *types.M } // delegate back the unbonding delegation amount to the validator - _, err = k.Keeper.Delegate(ctx, delegatorAddress, msg.Amount.Amount, types.Unbonding, validator, false) + newShares, err := k.Keeper.Delegate(ctx, delegatorAddress, msg.Amount.Amount, types.Unbonding, validator, false) if err != nil { return nil, err } + // If the delegation is a validator bond, increment the validator bond shares + delegation, found := k.Keeper.GetDelegation(ctx, delegatorAddress, valAddr) + if !found { + return nil, types.ErrNoDelegation + } + if delegation.ValidatorBond { + if err := k.IncreaseValidatorBondShares(ctx, valAddr, newShares); err != nil { + return nil, err + } + } + amount := unbondEntry.Balance.Sub(msg.Amount.Amount) if amount.IsZero() { ubd.RemoveEntry(unbondEntryIndex) @@ -454,11 +604,11 @@ func (k msgServer) CancelUnbondingDelegation(goCtx context.Context, msg *types.M ctx.EventManager().EmitEvent( sdk.NewEvent( - types.EventTypeCancelUnbondingDelegation, + "cancel_unbonding_delegation", sdk.NewAttribute(sdk.AttributeKeyAmount, msg.Amount.String()), sdk.NewAttribute(types.AttributeKeyValidator, msg.ValidatorAddress), sdk.NewAttribute(types.AttributeKeyDelegator, msg.DelegatorAddress), - sdk.NewAttribute(types.AttributeKeyCreationHeight, strconv.FormatInt(msg.CreationHeight, 10)), + sdk.NewAttribute("creation_height", strconv.FormatInt(msg.CreationHeight, 10)), ), ) @@ -485,15 +635,180 @@ func (ms msgServer) UpdateParams(goCtx context.Context, msg *types.MsgUpdatePara // This allows a validator to stop their services and jail themselves without // experiencing a slash func (k msgServer) UnbondValidator(goCtx context.Context, msg *types.MsgUnbondValidator) (*types.MsgUnbondValidatorResponse, error) { - // TODO add LSM logic + ctx := sdk.UnwrapSDKContext(goCtx) + valAddr, err := sdk.ValAddressFromBech32(msg.ValidatorAddress) + if err != nil { + return nil, err + } + // validator must already be registered + validator, found := k.GetValidator(ctx, valAddr) + if !found { + return nil, types.ErrNoValidatorFound + } + + // jail the validator. + k.jailValidator(ctx, validator) return &types.MsgUnbondValidatorResponse{}, nil } // Tokenizes shares associated with a delegation by creating a tokenize share record // and returning tokens with a denom of the format {validatorAddress}/{recordId} func (k msgServer) TokenizeShares(goCtx context.Context, msg *types.MsgTokenizeShares) (*types.MsgTokenizeSharesResponse, error) { - shareToken := sdk.Coin{} - // TODO add LSM logic + ctx := sdk.UnwrapSDKContext(goCtx) + + valAddr, valErr := sdk.ValAddressFromBech32(msg.ValidatorAddress) + if valErr != nil { + return nil, valErr + } + validator, found := k.GetValidator(ctx, valAddr) + if !found { + return nil, types.ErrNoValidatorFound + } + + delegatorAddress, err := sdk.AccAddressFromBech32(msg.DelegatorAddress) + if err != nil { + return nil, err + } + + // Check if the delegator has disabled tokenization + lockStatus, unlockTime := k.GetTokenizeSharesLock(ctx, delegatorAddress) + if lockStatus == types.TOKENIZE_SHARE_LOCK_STATUS_LOCKED { + return nil, types.ErrTokenizeSharesDisabledForAccount + } + if lockStatus == types.TOKENIZE_SHARE_LOCK_STATUS_LOCK_EXPIRING { + return nil, types.ErrTokenizeSharesDisabledForAccount.Wrapf("tokenization will be allowed at %s", unlockTime) + } + + delegation, found := k.GetDelegation(ctx, delegatorAddress, valAddr) + if !found { + return nil, types.ErrNoDelegatorForAddress + } + + if delegation.ValidatorBond { + return nil, types.ErrValidatorBondNotAllowedForTokenizeShare + } + + if msg.Amount.Denom != k.BondDenom(ctx) { + return nil, types.ErrOnlyBondDenomAllowdForTokenize + } + + acc := k.authKeeper.GetAccount(ctx, delegatorAddress) + if acc != nil { + acc, ok := acc.(vesting.VestingAccount) + if ok { + // if account is a vesting account, it checks if free delegation (non-vesting delegation) is not exceeding + // the tokenize share amount and execute further tokenize share process + // tokenize share is reducing unlocked tokens delegation from the vesting account and further process + // is not causing issues + delFree := acc.GetDelegatedFree().AmountOf(msg.Amount.Denom) + if delFree.LT(msg.Amount.Amount) { + return nil, types.ErrExceedingFreeVestingDelegations + } + } + } + + shares, err := k.ValidateUnbondAmount( + ctx, delegatorAddress, valAddr, msg.Amount.Amount, + ) + if err != nil { + return nil, err + } + + // If this tokenization is NOT from a liquid staking provider, + // confirm it does not exceed the global and validator liquid staking cap + // If the tokenization is from a liquid staking provider, + // the shares are already considered liquid and there's no need to increment the totals + if !k.DelegatorIsLiquidStaker(delegatorAddress) { + if err := k.SafelyIncreaseTotalLiquidStakedTokens(ctx, msg.Amount.Amount, true); err != nil { + return nil, err + } + validator, err = k.SafelyIncreaseValidatorLiquidShares(ctx, valAddr, shares) + if err != nil { + return nil, err + } + } + + recordID := k.GetLastTokenizeShareRecordID(ctx) + 1 + k.SetLastTokenizeShareRecordID(ctx, recordID) + + record := types.TokenizeShareRecord{ + Id: recordID, + Owner: msg.TokenizedShareOwner, + ModuleAccount: fmt.Sprintf("%s%d", types.TokenizeShareModuleAccountPrefix, recordID), + Validator: msg.ValidatorAddress, + } + + // note: this returnAmount can be slightly off from the original delegation amount if there + // is a decimal to int precision error + returnAmount, err := k.Unbond(ctx, delegatorAddress, valAddr, shares) + if err != nil { + return nil, err + } + + if validator.IsBonded() { + k.bondedTokensToNotBonded(ctx, returnAmount) + } + + // Note: UndelegateCoinsFromModuleToAccount is internally calling TrackUndelegation for vesting account + returnCoin := sdk.NewCoin(k.BondDenom(ctx), returnAmount) + err = k.bankKeeper.UndelegateCoinsFromModuleToAccount(ctx, types.NotBondedPoolName, delegatorAddress, sdk.Coins{returnCoin}) + if err != nil { + return nil, err + } + + // Re-calculate the shares in case there was rounding precision during the undelegation + newShares, err := validator.SharesFromTokens(returnAmount) + if err != nil { + return nil, err + } + + // The share tokens returned maps 1:1 with shares + shareToken := sdk.NewCoin(record.GetShareTokenDenom(), newShares.TruncateInt()) + + err = k.bankKeeper.MintCoins(ctx, minttypes.ModuleName, sdk.Coins{shareToken}) + if err != nil { + return nil, err + } + + err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, delegatorAddress, sdk.Coins{shareToken}) + if err != nil { + return nil, err + } + + // create reward ownership record + err = k.AddTokenizeShareRecord(ctx, record) + if err != nil { + return nil, err + } + // send coins to module account + err = k.bankKeeper.SendCoins(ctx, delegatorAddress, record.GetModuleAddress(), sdk.Coins{returnCoin}) + if err != nil { + return nil, err + } + + // Note: it is needed to get latest validator object to get Keeper.Delegate function work properly + validator, found = k.GetValidator(ctx, valAddr) + if !found { + return nil, types.ErrNoValidatorFound + } + + // delegate from module account + _, err = k.Keeper.Delegate(ctx, record.GetModuleAddress(), returnAmount, types.Unbonded, validator, true) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeTokenizeShares, + sdk.NewAttribute(types.AttributeKeyDelegator, msg.DelegatorAddress), + sdk.NewAttribute(types.AttributeKeyValidator, msg.ValidatorAddress), + sdk.NewAttribute(types.AttributeKeyShareOwner, msg.TokenizedShareOwner), + sdk.NewAttribute(types.AttributeKeyShareRecordID, fmt.Sprintf("%d", record.Id)), + sdk.NewAttribute(types.AttributeKeyAmount, msg.Amount.String()), + ), + ) + return &types.MsgTokenizeSharesResponse{ Amount: shareToken, }, nil @@ -501,8 +816,123 @@ func (k msgServer) TokenizeShares(goCtx context.Context, msg *types.MsgTokenizeS // Converts tokenized shares back into a native delegation func (k msgServer) RedeemTokensForShares(goCtx context.Context, msg *types.MsgRedeemTokensForShares) (*types.MsgRedeemTokensForSharesResponse, error) { - returnCoin := sdk.Coin{} - // TODO add LSM logic + ctx := sdk.UnwrapSDKContext(goCtx) + + delegatorAddress, err := sdk.AccAddressFromBech32(msg.DelegatorAddress) + if err != nil { + return nil, err + } + + shareToken := msg.Amount + balance := k.bankKeeper.GetBalance(ctx, delegatorAddress, shareToken.Denom) + if balance.Amount.LT(shareToken.Amount) { + return nil, types.ErrNotEnoughBalance + } + + record, err := k.GetTokenizeShareRecordByDenom(ctx, shareToken.Denom) + if err != nil { + return nil, err + } + + valAddr, valErr := sdk.ValAddressFromBech32(record.Validator) + if valErr != nil { + return nil, valErr + } + + validator, found := k.GetValidator(ctx, valAddr) + if !found { + return nil, types.ErrNoValidatorFound + } + + delegation, found := k.GetDelegation(ctx, record.GetModuleAddress(), valAddr) + if !found { + return nil, types.ErrNoUnbondingDelegation + } + + // Similar to undelegations, if the account is attempting to tokenize the full delegation, + // but there's a precision error due to the decimal to int conversion, round up to the + // full decimal amount before modifying the delegation + shares := shareToken.Amount.ToDec() + if shareToken.Amount.Equal(delegation.Shares.TruncateInt()) { + shares = delegation.Shares + } + tokens := validator.TokensFromShares(shares).TruncateInt() + + // If this redemption is NOT from a liquid staking provider, decrement the total liquid staked + // If the redemption was from a liquid staking provider, the shares are still considered + // liquid, even in their non-tokenized form (since they are owned by a liquid staking provider) + if !k.DelegatorIsLiquidStaker(delegatorAddress) { + if err := k.DecreaseTotalLiquidStakedTokens(ctx, tokens); err != nil { + return nil, err + } + validator, err = k.DecreaseValidatorLiquidShares(ctx, valAddr, shares) + if err != nil { + return nil, err + } + } + + returnAmount, err := k.Unbond(ctx, record.GetModuleAddress(), valAddr, shares) + if err != nil { + return nil, err + } + + if validator.IsBonded() { + k.bondedTokensToNotBonded(ctx, returnAmount) + } + + // Note: since delegation object has been changed from unbond call, it gets latest delegation + _, found = k.GetDelegation(ctx, record.GetModuleAddress(), valAddr) + if !found { + if k.hooks != nil { + if err := k.hooks.BeforeTokenizeShareRecordRemoved(ctx, record.Id); err != nil { + return nil, err + } + } + err = k.DeleteTokenizeShareRecord(ctx, record.Id) + if err != nil { + return nil, err + } + } + + // send share tokens to NotBondedPool and burn + err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, delegatorAddress, types.NotBondedPoolName, sdk.Coins{shareToken}) + if err != nil { + return nil, err + } + err = k.bankKeeper.BurnCoins(ctx, types.NotBondedPoolName, sdk.Coins{shareToken}) + if err != nil { + return nil, err + } + + // send equivalent amount of tokens to the delegator + returnCoin := sdk.NewCoin(k.BondDenom(ctx), returnAmount) + err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.NotBondedPoolName, delegatorAddress, sdk.Coins{returnCoin}) + if err != nil { + return nil, err + } + + // Note: it is needed to get latest validator object to get Keeper.Delegate function work properly + validator, found = k.GetValidator(ctx, valAddr) + if !found { + return nil, types.ErrNoValidatorFound + } + + // convert the share tokens to delegated status + // Note: Delegate(substractAccount => true) -> DelegateCoinsFromAccountToModule -> TrackDelegation for vesting account + _, err = k.Keeper.Delegate(ctx, delegatorAddress, returnAmount, types.Unbonded, validator, true) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeRedeemShares, + sdk.NewAttribute(types.AttributeKeyDelegator, msg.DelegatorAddress), + sdk.NewAttribute(types.AttributeKeyValidator, validator.OperatorAddress), + sdk.NewAttribute(types.AttributeKeyAmount, shareToken.String()), + ), + ) + return &types.MsgRedeemTokensForSharesResponse{ Amount: returnCoin, }, nil @@ -510,27 +940,137 @@ func (k msgServer) RedeemTokensForShares(goCtx context.Context, msg *types.MsgRe // Transfers the ownership of rewards associated with a tokenize share record func (k msgServer) TransferTokenizeShareRecord(goCtx context.Context, msg *types.MsgTransferTokenizeShareRecord) (*types.MsgTransferTokenizeShareRecordResponse, error) { - // TODO add LSM logic + ctx := sdk.UnwrapSDKContext(goCtx) + + record, err := k.GetTokenizeShareRecord(ctx, msg.TokenizeShareRecordId) + if err != nil { + return nil, types.ErrTokenizeShareRecordNotExists + } + + if record.Owner != msg.Sender { + return nil, types.ErrNotTokenizeShareRecordOwner + } + + // Remove old account reference + oldOwner, err := sdk.AccAddressFromBech32(record.Owner) + if err != nil { + return nil, sdkerrors.ErrInvalidAddress + } + k.deleteTokenizeShareRecordWithOwner(ctx, oldOwner, record.Id) + + record.Owner = msg.NewOwner + k.setTokenizeShareRecord(ctx, record) + + // Set new account reference + newOwner, err := sdk.AccAddressFromBech32(record.Owner) + if err != nil { + return nil, sdkerrors.ErrInvalidAddress + } + k.setTokenizeShareRecordWithOwner(ctx, newOwner, record.Id) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeTransferTokenizeShareRecord, + sdk.NewAttribute(types.AttributeKeyShareRecordID, fmt.Sprintf("%d", msg.TokenizeShareRecordId)), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender), + sdk.NewAttribute(types.AttributeKeyShareOwner, msg.NewOwner), + ), + ) + return &types.MsgTransferTokenizeShareRecordResponse{}, nil } // DisableTokenizeShares prevents an address from tokenizing any of their delegations func (k msgServer) DisableTokenizeShares(goCtx context.Context, msg *types.MsgDisableTokenizeShares) (*types.MsgDisableTokenizeSharesResponse, error) { - // TODO add LSM logic + ctx := sdk.UnwrapSDKContext(goCtx) + + delegator := sdk.MustAccAddressFromBech32(msg.DelegatorAddress) + + // If tokenized shares is already disabled, alert the user + lockStatus, completionTime := k.GetTokenizeSharesLock(ctx, delegator) + if lockStatus == types.TOKENIZE_SHARE_LOCK_STATUS_LOCKED { + return nil, types.ErrTokenizeSharesAlreadyDisabledForAccount + } + + // If the tokenized shares lock is expiring, remove the pending unlock from the queue + if lockStatus == types.TOKENIZE_SHARE_LOCK_STATUS_LOCK_EXPIRING { + k.CancelTokenizeShareLockExpiration(ctx, delegator, completionTime) + } + + // Create a new tokenization lock for the user + // Note: if there is a lock expiration in progress, this will override the expiration + k.AddTokenizeSharesLock(ctx, delegator) + return &types.MsgDisableTokenizeSharesResponse{}, nil } // EnableTokenizeShares begins the countdown after which tokenizing shares by the // sender address is re-allowed, which will complete after the unbonding period func (k msgServer) EnableTokenizeShares(goCtx context.Context, msg *types.MsgEnableTokenizeShares) (*types.MsgEnableTokenizeSharesResponse, error) { - completionTime := time.Time{} - // TODO add LSM logic + ctx := sdk.UnwrapSDKContext(goCtx) + + delegator := sdk.MustAccAddressFromBech32(msg.DelegatorAddress) + + // If tokenized shares aren't current disabled, alert the user + lockStatus, unlockTime := k.GetTokenizeSharesLock(ctx, delegator) + if lockStatus == types.TOKENIZE_SHARE_LOCK_STATUS_UNLOCKED { + return nil, types.ErrTokenizeSharesAlreadyEnabledForAccount + } + if lockStatus == types.TOKENIZE_SHARE_LOCK_STATUS_LOCK_EXPIRING { + return nil, types.ErrTokenizeSharesAlreadyEnabledForAccount.Wrapf( + "tokenize shares re-enablement already in progress, ending at %s", unlockTime) + } + + // Otherwise queue the unlock + completionTime := k.QueueTokenizeSharesAuthorization(ctx, delegator) + return &types.MsgEnableTokenizeSharesResponse{CompletionTime: completionTime}, nil } // Designates a delegation as a validator bond // This enables the validator to receive more liquid staking delegations func (k msgServer) ValidatorBond(goCtx context.Context, msg *types.MsgValidatorBond) (*types.MsgValidatorBondResponse, error) { - // TODO add LSM logic + ctx := sdk.UnwrapSDKContext(goCtx) + + delAddr, err := sdk.AccAddressFromBech32(msg.DelegatorAddress) + if err != nil { + return nil, err + } + + valAddr, valErr := sdk.ValAddressFromBech32(msg.ValidatorAddress) + if valErr != nil { + return nil, valErr + } + + validator, found := k.GetValidator(ctx, valAddr) + if !found { + return nil, types.ErrNoValidatorFound + } + + delegation, found := k.GetDelegation(ctx, delAddr, valAddr) + if !found { + return nil, types.ErrNoDelegation + } + + // liquid staking providers should not be able to validator bond + if k.DelegatorIsLiquidStaker(delAddr) { + return nil, types.ErrValidatorBondNotAllowedFromModuleAccount + } + + if !delegation.ValidatorBond { + delegation.ValidatorBond = true + k.SetDelegation(ctx, delegation) + validator.ValidatorBondShares = validator.ValidatorBondShares.Add(delegation.Shares) + k.SetValidator(ctx, validator) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeValidatorBondDelegation, + sdk.NewAttribute(types.AttributeKeyDelegator, msg.DelegatorAddress), + sdk.NewAttribute(types.AttributeKeyValidator, msg.ValidatorAddress), + ), + ) + } + return &types.MsgValidatorBondResponse{}, nil } diff --git a/x/staking/keeper/params.go b/x/staking/keeper/params.go index 3e1bb083916e..30b250dd672f 100644 --- a/x/staking/keeper/params.go +++ b/x/staking/keeper/params.go @@ -44,6 +44,23 @@ func (k Keeper) PowerReduction(ctx sdk.Context) math.Int { return sdk.DefaultPowerReduction } +// Validator bond factor for all validators +func (k Keeper) ValidatorBondFactor(ctx sdk.Context) (res sdk.Dec) { + return k.GetParams(ctx).ValidatorBondFactor + +} + +// Global liquid staking cap across all liquid staking providers +func (k Keeper) GlobalLiquidStakingCap(ctx sdk.Context) (res sdk.Dec) { + return k.GetParams(ctx).GlobalLiquidStakingCap + +} + +// Liquid staking cap for each validator +func (k Keeper) ValidatorLiquidStakingCap(ctx sdk.Context) (res sdk.Dec) { + return k.GetParams(ctx).ValidatorLiquidStakingCap +} + // MinCommissionRate - Minimum validator commission rate func (k Keeper) MinCommissionRate(ctx sdk.Context) math.LegacyDec { return k.GetParams(ctx).MinCommissionRate diff --git a/x/staking/simulation/genesis.go b/x/staking/simulation/genesis.go index afd393c1c778..6e0c372a54d5 100644 --- a/x/staking/simulation/genesis.go +++ b/x/staking/simulation/genesis.go @@ -16,9 +16,12 @@ import ( // Simulation parameter constants const ( - unbondingTime = "unbonding_time" - maxValidators = "max_validators" - historicalEntries = "historical_entries" + unbondingTime = "unbonding_time" + maxValidators = "max_validators" + historicalEntries = "historical_entries" + ValidatorBondFactor = "validator_bond_factor" + GlobalLiquidStakingCap = "global_liquid_staking_cap" + ValidatorLiquidStakingCap = "validator_liquid_staking_cap" ) // genUnbondingTime returns randomized UnbondingTime @@ -40,10 +43,13 @@ func getHistEntries(r *rand.Rand) uint32 { func RandomizedGenState(simState *module.SimulationState) { // params var ( - unbondTime time.Duration - maxVals uint32 - histEntries uint32 - minCommissionRate sdk.Dec + unbondTime time.Duration + maxVals uint32 + histEntries uint32 + minCommissionRate sdk.Dec + validatorBondFactor sdk.Dec + globalLiquidStakingCap sdk.Dec + validatorLiquidStakingCap sdk.Dec ) simState.AppParams.GetOrGenerate( @@ -64,7 +70,15 @@ func RandomizedGenState(simState *module.SimulationState) { // NOTE: the slashing module need to be defined after the staking module on the // NewSimulationManager constructor for this to work simState.UnbondTime = unbondTime - params := types.NewParams(simState.UnbondTime, maxVals, 7, histEntries, sdk.DefaultBondDenom, minCommissionRate) + params := types.NewParams(simState.UnbondTime, maxVals, + 7, + histEntries, + sdk.DefaultBondDenom, + minCommissionRate, + validatorBondFactor, + globalLiquidStakingCap, + validatorLiquidStakingCap, + ) // validators & delegations var ( diff --git a/x/staking/testutil/expected_keepers_mocks.go b/x/staking/testutil/expected_keepers_mocks.go index 375c6ff3c729..e15eb1eb57ed 100644 --- a/x/staking/testutil/expected_keepers_mocks.go +++ b/x/staking/testutil/expected_keepers_mocks.go @@ -261,6 +261,48 @@ func (mr *MockBankKeeperMockRecorder) LockedCoins(ctx, addr interface{}) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LockedCoins", reflect.TypeOf((*MockBankKeeper)(nil).LockedCoins), ctx, addr) } +// MintCoins mocks base method. +func (m *MockBankKeeper) MintCoins(cts types.Context, name string, amt types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MintCoins", cts, name, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// MintCoins indicates an expected call of MintCoins. +func (mr *MockBankKeeperMockRecorder) MintCoins(cts, name, amt interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MintCoins", reflect.TypeOf((*MockBankKeeper)(nil).MintCoins), cts, name, amt) +} + +// SendCoins mocks base method. +func (m *MockBankKeeper) SendCoins(ctx types.Context, fromAddr, toAddr types.AccAddress, amt types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendCoins", ctx, fromAddr, toAddr, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendCoins indicates an expected call of SendCoins. +func (mr *MockBankKeeperMockRecorder) SendCoins(ctx, fromAddr, toAddr, amt interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoins", reflect.TypeOf((*MockBankKeeper)(nil).SendCoins), ctx, fromAddr, toAddr, amt) +} + +// SendCoinsFromModuleToAccount mocks base method. +func (m *MockBankKeeper) SendCoinsFromModuleToAccount(ctx types.Context, senderModule string, recipientAddr types.AccAddress, amt types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendCoinsFromModuleToAccount", ctx, senderModule, recipientAddr, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendCoinsFromModuleToAccount indicates an expected call of SendCoinsFromModuleToAccount. +func (mr *MockBankKeeperMockRecorder) SendCoinsFromModuleToAccount(ctx, senderModule, recipientAddr, amt interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoinsFromModuleToAccount", reflect.TypeOf((*MockBankKeeper)(nil).SendCoinsFromModuleToAccount), ctx, senderModule, recipientAddr, amt) +} + // SendCoinsFromModuleToModule mocks base method. func (m *MockBankKeeper) SendCoinsFromModuleToModule(ctx types.Context, senderPool, recipientPool string, amt types.Coins) error { m.ctrl.T.Helper() @@ -696,6 +738,20 @@ func (mr *MockStakingHooksMockRecorder) BeforeDelegationSharesModified(ctx, delA return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeforeDelegationSharesModified", reflect.TypeOf((*MockStakingHooks)(nil).BeforeDelegationSharesModified), ctx, delAddr, valAddr) } +// BeforeTokenizeShareRecordRemoved mocks base method. +func (m *MockStakingHooks) BeforeTokenizeShareRecordRemoved(ctx types.Context, recordID uint64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BeforeTokenizeShareRecordRemoved", ctx, recordID) + ret0, _ := ret[0].(error) + return ret0 +} + +// BeforeTokenizeShareRecordRemoved indicates an expected call of BeforeTokenizeShareRecordRemoved. +func (mr *MockStakingHooksMockRecorder) BeforeTokenizeShareRecordRemoved(ctx, recordID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeforeTokenizeShareRecordRemoved", reflect.TypeOf((*MockStakingHooks)(nil).BeforeTokenizeShareRecordRemoved), ctx, recordID) +} + // BeforeValidatorModified mocks base method. func (m *MockStakingHooks) BeforeValidatorModified(ctx types.Context, valAddr types.ValAddress) error { m.ctrl.T.Helper() diff --git a/x/staking/types/events.go b/x/staking/types/events.go index 5195955feae7..431535399094 100644 --- a/x/staking/types/events.go +++ b/x/staking/types/events.go @@ -2,14 +2,18 @@ package types // staking module event types const ( - EventTypeCompleteUnbonding = "complete_unbonding" - EventTypeCompleteRedelegation = "complete_redelegation" - EventTypeCreateValidator = "create_validator" - EventTypeEditValidator = "edit_validator" - EventTypeDelegate = "delegate" - EventTypeUnbond = "unbond" - EventTypeCancelUnbondingDelegation = "cancel_unbonding_delegation" - EventTypeRedelegate = "redelegate" + EventTypeCompleteUnbonding = "complete_unbonding" + EventTypeCompleteRedelegation = "complete_redelegation" + EventTypeCreateValidator = "create_validator" + EventTypeEditValidator = "edit_validator" + EventTypeDelegate = "delegate" + EventTypeUnbond = "unbond" + EventTypeCancelUnbondingDelegation = "cancel_unbonding_delegation" + EventTypeRedelegate = "redelegate" + EventTypeTokenizeShares = "tokenize_shares" + EventTypeRedeemShares = "redeem_shares" + EventTypeTransferTokenizeShareRecord = "transfer_tokenize_share_record" + EventTypeValidatorBondDelegation = "validator_bond_delegation" AttributeKeyValidator = "validator" AttributeKeyCommissionRate = "commission_rate" @@ -19,4 +23,7 @@ const ( AttributeKeyCreationHeight = "creation_height" AttributeKeyCompletionTime = "completion_time" AttributeKeyNewShares = "new_shares" + AttributeKeyShareOwner = "share_owner" + AttributeKeyShareRecordID = "share_record_id" + AttributeKeyAmount = "amount" ) diff --git a/x/staking/types/expected_keepers.go b/x/staking/types/expected_keepers.go index 05672ee256da..0220dfa1e79f 100644 --- a/x/staking/types/expected_keepers.go +++ b/x/staking/types/expected_keepers.go @@ -34,10 +34,13 @@ type BankKeeper interface { GetSupply(ctx sdk.Context, denom string) sdk.Coin + SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error SendCoinsFromModuleToModule(ctx sdk.Context, senderPool, recipientPool string, amt sdk.Coins) error + SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error UndelegateCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error DelegateCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error + MintCoins(cts sdk.Context, name string, amt sdk.Coins) error BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) error } @@ -105,6 +108,7 @@ type StakingHooks interface { AfterDelegationModified(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) error BeforeValidatorSlashed(ctx sdk.Context, valAddr sdk.ValAddress, fraction sdk.Dec) error AfterUnbondingInitiated(ctx sdk.Context, id uint64) error + BeforeTokenizeShareRecordRemoved(ctx sdk.Context, recordID uint64) error // Must be called when tokenize share record is deleted } // StakingHooksWrapper is a wrapper for modules to inject StakingHooks using depinject. diff --git a/x/staking/types/hooks.go b/x/staking/types/hooks.go index 6fad1df77d6b..cee47afbaad3 100644 --- a/x/staking/types/hooks.go +++ b/x/staking/types/hooks.go @@ -112,3 +112,12 @@ func (h MultiStakingHooks) AfterUnbondingInitiated(ctx sdk.Context, id uint64) e } return nil } + +func (h MultiStakingHooks) BeforeTokenizeShareRecordRemoved(ctx sdk.Context, recordID uint64) error { + for i := range h { + if err := h[i].BeforeTokenizeShareRecordRemoved(ctx, recordID); err != nil { + return err + } + } + return nil +} diff --git a/x/staking/types/msg.go b/x/staking/types/msg.go index 553f49bf8570..e435c1069c70 100644 --- a/x/staking/types/msg.go +++ b/x/staking/types/msg.go @@ -10,13 +10,20 @@ import ( // staking message types const ( - TypeMsgUndelegate = "begin_unbonding" - TypeMsgCancelUnbondingDelegation = "cancel_unbond" - TypeMsgEditValidator = "edit_validator" - TypeMsgCreateValidator = "create_validator" - TypeMsgDelegate = "delegate" - TypeMsgBeginRedelegate = "begin_redelegate" - TypeMsgUpdateParams = "update_params" + TypeMsgUndelegate = "begin_unbonding" + TypeMsgUnbondValidator = "unbond_validator" + TypeMsgCancelUnbondingDelegation = "cancel_unbond" + TypeMsgEditValidator = "edit_validator" + TypeMsgCreateValidator = "create_validator" + TypeMsgDelegate = "delegate" + TypeMsgBeginRedelegate = "begin_redelegate" + TypeMsgUpdateParams = "update_params" + TypeMsgTokenizeShares = "tokenize_shares" + TypeMsgRedeemTokensForShares = "redeem_tokens_for_shares" + TypeMsgTransferTokenizeShareRecord = "transfer_tokenize_share_record" + TypeMsgDisableTokenizeShares = "disable_tokenize_shares" + TypeMsgEnableTokenizeShares = "enable_tokenize_shares" + TypeMsgValidatorBond = "validator_bond" ) var ( @@ -401,3 +408,309 @@ func (m *MsgUpdateParams) GetSigners() []sdk.AccAddress { addr, _ := sdk.AccAddressFromBech32(m.Authority) return []sdk.AccAddress{addr} } + +// NewMsgUnbondValidator creates a new MsgUnbondValidator instance. +// +//nolint:interfacer +func NewMsgUnbondValidator(valAddr sdk.ValAddress) *MsgUnbondValidator { + return &MsgUnbondValidator{ + ValidatorAddress: valAddr.String(), + } +} + +// Route implements the sdk.Msg interface. +func (msg MsgUnbondValidator) Route() string { return RouterKey } + +// Type implements the sdk.Msg interface. +func (msg MsgUnbondValidator) Type() string { return TypeMsgUnbondValidator } + +// GetSigners implements the sdk.Msg interface. +func (msg MsgUnbondValidator) GetSigners() []sdk.AccAddress { + valAddr, err := sdk.ValAddressFromBech32(msg.ValidatorAddress) + if err != nil { + panic(err) + } + return []sdk.AccAddress{valAddr.Bytes()} +} + +// GetSignBytes implements the sdk.Msg interface. +func (msg MsgUnbondValidator) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(&msg) + return sdk.MustSortJSON(bz) +} + +// ValidateBasic implements the sdk.Msg interface. +func (msg MsgUnbondValidator) ValidateBasic() error { + if _, err := sdk.ValAddressFromBech32(msg.ValidatorAddress); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid validator address: %s", err) + } + + return nil +} + +// NewMsgTokenizeShares creates a new MsgTokenizeShares instance. +// +//nolint:interfacer +func NewMsgTokenizeShares(delAddr sdk.AccAddress, valAddr sdk.ValAddress, amount sdk.Coin, owner sdk.AccAddress) *MsgTokenizeShares { + return &MsgTokenizeShares{ + DelegatorAddress: delAddr.String(), + ValidatorAddress: valAddr.String(), + Amount: amount, + TokenizedShareOwner: owner.String(), + } +} + +// Route implements the sdk.Msg interface. +func (msg MsgTokenizeShares) Route() string { return RouterKey } + +// Type implements the sdk.Msg interface. +func (msg MsgTokenizeShares) Type() string { return TypeMsgTokenizeShares } + +// GetSigners implements the sdk.Msg interface. +func (msg MsgTokenizeShares) GetSigners() []sdk.AccAddress { + delegator, err := sdk.AccAddressFromBech32(msg.DelegatorAddress) + if err != nil { + panic(err) + } + return []sdk.AccAddress{delegator} +} + +// MsgTokenizeShares implements the sdk.Msg interface. +func (msg MsgTokenizeShares) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(&msg) + return sdk.MustSortJSON(bz) +} + +// ValidateBasic implements the sdk.Msg interface. +func (msg MsgTokenizeShares) ValidateBasic() error { + if _, err := sdk.AccAddressFromBech32(msg.DelegatorAddress); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid delegator address: %s", err) + } + if _, err := sdk.ValAddressFromBech32(msg.ValidatorAddress); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid validator address: %s", err) + } + if _, err := sdk.AccAddressFromBech32(msg.TokenizedShareOwner); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid tokenize share owner address: %s", err) + } + + if !msg.Amount.IsValid() || !msg.Amount.Amount.IsPositive() { + return sdkerrors.Wrap( + sdkerrors.ErrInvalidRequest, + "invalid shares amount", + ) + } + + return nil +} + +// NewMsgRedeemTokensForShares creates a new MsgRedeemTokensForShares instance. +// +//nolint:interfacer +func NewMsgRedeemTokensForShares(delAddr sdk.AccAddress, amount sdk.Coin) *MsgRedeemTokensForShares { + return &MsgRedeemTokensForShares{ + DelegatorAddress: delAddr.String(), + Amount: amount, + } +} + +// Route implements the sdk.Msg interface. +func (msg MsgRedeemTokensForShares) Route() string { return RouterKey } + +// Type implements the sdk.Msg interface. +func (msg MsgRedeemTokensForShares) Type() string { return TypeMsgRedeemTokensForShares } + +// GetSigners implements the sdk.Msg interface. +func (msg MsgRedeemTokensForShares) GetSigners() []sdk.AccAddress { + delegator, err := sdk.AccAddressFromBech32(msg.DelegatorAddress) + if err != nil { + panic(err) + } + return []sdk.AccAddress{delegator} +} + +// GetSignBytes implements the sdk.Msg interface. +func (msg MsgRedeemTokensForShares) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(&msg) + return sdk.MustSortJSON(bz) +} + +// ValidateBasic implements the sdk.Msg interface. +func (msg MsgRedeemTokensForShares) ValidateBasic() error { + if _, err := sdk.AccAddressFromBech32(msg.DelegatorAddress); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid delegator address: %s", err) + } + + if !msg.Amount.IsValid() || !msg.Amount.Amount.IsPositive() { + return sdkerrors.Wrap( + sdkerrors.ErrInvalidRequest, + "invalid shares amount", + ) + } + + return nil +} + +// NewMsgTransferTokenizeShareRecord creates a new MsgTransferTokenizeShareRecord instance. +// +//nolint:interfacer +func NewMsgTransferTokenizeShareRecord(recordId uint64, sender, newOwner sdk.AccAddress) *MsgTransferTokenizeShareRecord { + return &MsgTransferTokenizeShareRecord{ + TokenizeShareRecordId: recordId, + Sender: sender.String(), + NewOwner: newOwner.String(), + } +} + +// Route implements the sdk.Msg interface. +func (msg MsgTransferTokenizeShareRecord) Route() string { return RouterKey } + +// Type implements the sdk.Msg interface. +func (msg MsgTransferTokenizeShareRecord) Type() string { return TypeMsgTransferTokenizeShareRecord } + +// GetSigners implements the sdk.Msg interface. +func (msg MsgTransferTokenizeShareRecord) GetSigners() []sdk.AccAddress { + sender, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + panic(err) + } + return []sdk.AccAddress{sender} +} + +// GetSignBytes implements the sdk.Msg interface. +func (msg MsgTransferTokenizeShareRecord) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(&msg) + return sdk.MustSortJSON(bz) +} + +// ValidateBasic implements the sdk.Msg interface. +func (msg MsgTransferTokenizeShareRecord) ValidateBasic() error { + if _, err := sdk.AccAddressFromBech32(msg.Sender); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid sender address: %s", err) + } + if _, err := sdk.AccAddressFromBech32(msg.NewOwner); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid new owner address: %s", err) + } + + return nil +} + +// NewMsgDisableTokenizeShares creates a new MsgDisableTokenizeShares instance. +// +//nolint:interfacer +func NewMsgDisableTokenizeShares(delAddr sdk.AccAddress) *MsgDisableTokenizeShares { + return &MsgDisableTokenizeShares{ + DelegatorAddress: delAddr.String(), + } +} + +// Route implements the sdk.Msg interface. +func (msg MsgDisableTokenizeShares) Route() string { return RouterKey } + +// Type implements the sdk.Msg interface. +func (msg MsgDisableTokenizeShares) Type() string { return TypeMsgDisableTokenizeShares } + +// GetSigners implements the sdk.Msg interface. +func (msg MsgDisableTokenizeShares) GetSigners() []sdk.AccAddress { + sender, err := sdk.AccAddressFromBech32(msg.DelegatorAddress) + if err != nil { + panic(err) + } + return []sdk.AccAddress{sender} +} + +// GetSignBytes implements the sdk.Msg interface. +func (msg MsgDisableTokenizeShares) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(&msg) + return sdk.MustSortJSON(bz) +} + +// ValidateBasic implements the sdk.Msg interface. +func (msg MsgDisableTokenizeShares) ValidateBasic() error { + if _, err := sdk.AccAddressFromBech32(msg.DelegatorAddress); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid sender address: %s", err) + } + + return nil +} + +// NewMsgEnableTokenizeShares creates a new MsgEnableTokenizeShares instance. +// +//nolint:interfacer +func NewMsgEnableTokenizeShares(delAddr sdk.AccAddress) *MsgEnableTokenizeShares { + return &MsgEnableTokenizeShares{ + DelegatorAddress: delAddr.String(), + } +} + +// Route implements the sdk.Msg interface. +func (msg MsgEnableTokenizeShares) Route() string { return RouterKey } + +// Type implements the sdk.Msg interface. +func (msg MsgEnableTokenizeShares) Type() string { return TypeMsgEnableTokenizeShares } + +// GetSigners implements the sdk.Msg interface. +func (msg MsgEnableTokenizeShares) GetSigners() []sdk.AccAddress { + sender, err := sdk.AccAddressFromBech32(msg.DelegatorAddress) + if err != nil { + panic(err) + } + return []sdk.AccAddress{sender} +} + +// GetSignBytes implements the sdk.Msg interface. +func (msg MsgEnableTokenizeShares) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(&msg) + return sdk.MustSortJSON(bz) +} + +// ValidateBasic implements the sdk.Msg interface. +func (msg MsgEnableTokenizeShares) ValidateBasic() error { + if _, err := sdk.AccAddressFromBech32(msg.DelegatorAddress); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid sender address: %s", err) + } + + return nil +} + +// NewMsgValidatorBond creates a new MsgValidatorBond instance. +// +//nolint:interfacer +func NewMsgValidatorBond(delAddr sdk.AccAddress, valAddr sdk.ValAddress) *MsgValidatorBond { + return &MsgValidatorBond{ + DelegatorAddress: delAddr.String(), + ValidatorAddress: valAddr.String(), + } +} + +// Route implements the sdk.Msg interface. +func (msg MsgValidatorBond) Route() string { return RouterKey } + +// Type implements the sdk.Msg interface. +func (msg MsgValidatorBond) Type() string { return TypeMsgValidatorBond } + +// GetSigners implements the sdk.Msg interface. +func (msg MsgValidatorBond) GetSigners() []sdk.AccAddress { + delegator, err := sdk.AccAddressFromBech32(msg.DelegatorAddress) + if err != nil { + panic(err) + } + return []sdk.AccAddress{delegator} +} + +// GetSignBytes implements the sdk.Msg interface. +func (msg MsgValidatorBond) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(&msg) + return sdk.MustSortJSON(bz) +} + +// ValidateBasic implements the sdk.Msg interface. +func (msg MsgValidatorBond) ValidateBasic() error { + if _, err := sdk.AccAddressFromBech32(msg.DelegatorAddress); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid delegator address: %s", err) + } + if _, err := sdk.ValAddressFromBech32(msg.ValidatorAddress); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid validator address: %s", err) + } + + return nil +} diff --git a/x/staking/types/params.go b/x/staking/types/params.go index f4e24e3f1767..330132058f48 100644 --- a/x/staking/types/params.go +++ b/x/staking/types/params.go @@ -32,18 +32,43 @@ const ( DefaultHistoricalEntries uint32 = 10000 ) -// DefaultMinCommissionRate is set to 0% -var DefaultMinCommissionRate = math.LegacyZeroDec() +var ( + // DefaultMinCommissionRate is set to 0% + DefaultMinCommissionRate = math.LegacyZeroDec() + + // ValidatorBondFactor of -1 indicates that it's disabled + ValidatorBondCapDisabled = sdk.NewDecFromInt(sdk.NewInt(-1)) + + // DefaultValidatorBondFactor is set to -1 (disabled) + DefaultValidatorBondFactor = ValidatorBondCapDisabled + // DefaultGlobalLiquidStakingCap is set to 100% + DefaultGlobalLiquidStakingCap = sdk.OneDec() + // DefaultValidatorLiquidStakingCap is set to 100% + DefaultValidatorLiquidStakingCap = sdk.OneDec() +) // NewParams creates a new Params instance -func NewParams(unbondingTime time.Duration, maxValidators, maxEntries, historicalEntries uint32, bondDenom string, minCommissionRate sdk.Dec) Params { +func NewParams(unbondingTime time.Duration, + maxValidators, + maxEntries, + historicalEntries uint32, + bondDenom string, + minCommissionRate sdk.Dec, + validatorBondFactor sdk.Dec, + globalLiquidStakingCap sdk.Dec, + validatorLiquidStakingCap sdk.Dec, +) Params { return Params{ UnbondingTime: unbondingTime, MaxValidators: maxValidators, MaxEntries: maxEntries, HistoricalEntries: historicalEntries, - BondDenom: bondDenom, - MinCommissionRate: minCommissionRate, + + BondDenom: bondDenom, + MinCommissionRate: minCommissionRate, + ValidatorBondFactor: validatorBondFactor, + GlobalLiquidStakingCap: globalLiquidStakingCap, + ValidatorLiquidStakingCap: validatorLiquidStakingCap, } } @@ -56,6 +81,9 @@ func DefaultParams() Params { DefaultHistoricalEntries, sdk.DefaultBondDenom, DefaultMinCommissionRate, + DefaultValidatorBondFactor, + DefaultGlobalLiquidStakingCap, + DefaultValidatorLiquidStakingCap, ) } @@ -111,6 +139,18 @@ func (p Params) Validate() error { return err } + if err := validateValidatorBondFactor(p.ValidatorBondFactor); err != nil { + return err + } + + if err := validateGlobalLiquidStakingCap(p.GlobalLiquidStakingCap); err != nil { + return err + } + + if err := validateValidatorLiquidStakingCap(p.ValidatorLiquidStakingCap); err != nil { + return err + } + return nil } @@ -210,3 +250,48 @@ func validateMinCommissionRate(i interface{}) error { return nil } + +func validateValidatorBondFactor(i interface{}) error { + v, ok := i.(sdk.Dec) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + + if v.IsNegative() && !v.Equal(sdk.NewDec(-1)) { + return fmt.Errorf("invalid validator bond factor: %s", v) + } + + return nil +} + +func validateGlobalLiquidStakingCap(i interface{}) error { + v, ok := i.(sdk.Dec) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + + if v.IsNegative() { + return fmt.Errorf("global liquid staking cap cannot be negative: %s", v) + } + if v.GT(sdk.OneDec()) { + return fmt.Errorf("global liquid staking cap cannot be greater than 100%%: %s", v) + } + + return nil +} + +func validateValidatorLiquidStakingCap(i interface{}) error { + v, ok := i.(sdk.Dec) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + + if v.IsNegative() { + return fmt.Errorf("validator liquid staking cap cannot be negative: %s", v) + } + if v.GT(sdk.OneDec()) { + return fmt.Errorf("validator liquid staking cap cannot be greater than 100%%: %s", v) + } + + return nil +} diff --git a/x/staking/types/params_legacy.go b/x/staking/types/params_legacy.go index df474c02ffa1..95487c9408a4 100644 --- a/x/staking/types/params_legacy.go +++ b/x/staking/types/params_legacy.go @@ -3,12 +3,15 @@ package types import paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" var ( - KeyUnbondingTime = []byte("UnbondingTime") - KeyMaxValidators = []byte("MaxValidators") - KeyMaxEntries = []byte("MaxEntries") - KeyBondDenom = []byte("BondDenom") - KeyHistoricalEntries = []byte("HistoricalEntries") - KeyMinCommissionRate = []byte("MinCommissionRate") + KeyUnbondingTime = []byte("UnbondingTime") + KeyMaxValidators = []byte("MaxValidators") + KeyMaxEntries = []byte("MaxEntries") + KeyBondDenom = []byte("BondDenom") + KeyHistoricalEntries = []byte("HistoricalEntries") + KeyMinCommissionRate = []byte("MinCommissionRate") + KeyValidatorBondFactor = []byte("ValidatorBondFactor") + KeyGlobalLiquidStakingCap = []byte("GlobalLiquidStakingCap") + KeyValidatorLiquidStakingCap = []byte("ValidatorLiquidStakingCap") ) var _ paramtypes.ParamSet = (*Params)(nil) @@ -29,5 +32,8 @@ func (p *Params) ParamSetPairs() paramtypes.ParamSetPairs { paramtypes.NewParamSetPair(KeyHistoricalEntries, &p.HistoricalEntries, validateHistoricalEntries), paramtypes.NewParamSetPair(KeyBondDenom, &p.BondDenom, validateBondDenom), paramtypes.NewParamSetPair(KeyMinCommissionRate, &p.MinCommissionRate, validateMinCommissionRate), + paramtypes.NewParamSetPair(KeyValidatorBondFactor, &p.ValidatorBondFactor, validateValidatorBondFactor), + paramtypes.NewParamSetPair(KeyGlobalLiquidStakingCap, &p.GlobalLiquidStakingCap, validateGlobalLiquidStakingCap), + paramtypes.NewParamSetPair(KeyValidatorLiquidStakingCap, &p.ValidatorLiquidStakingCap, validateValidatorLiquidStakingCap), } } diff --git a/x/staking/types/validator.go b/x/staking/types/validator.go index 5c5ec4c1d3e5..f8467933380b 100644 --- a/x/staking/types/validator.go +++ b/x/staking/types/validator.go @@ -58,8 +58,9 @@ func NewValidator(operator sdk.ValAddress, pubKey cryptotypes.PubKey, descriptio UnbondingHeight: int64(0), UnbondingTime: time.Unix(0, 0).UTC(), Commission: NewCommission(math.LegacyZeroDec(), math.LegacyZeroDec(), math.LegacyZeroDec()), - MinSelfDelegation: math.OneInt(), UnbondingOnHoldRefCount: 0, + ValidatorBondShares: sdk.ZeroDec(), + LiquidShares: sdk.ZeroDec(), }, nil } @@ -454,7 +455,6 @@ func (v *Validator) MinEqual(other *Validator) bool { v.Description.Equal(other.Description) && v.Commission.Equal(other.Commission) && v.Jailed == other.Jailed && - v.MinSelfDelegation.Equal(other.MinSelfDelegation) && v.ConsensusPubkey.Equal(other.ConsensusPubkey) } @@ -520,8 +520,9 @@ func (v Validator) GetConsensusPower(r math.Int) int64 { return v.ConsensusPower(r) } func (v Validator) GetCommission() math.LegacyDec { return v.Commission.Rate } -func (v Validator) GetMinSelfDelegation() math.Int { return v.MinSelfDelegation } +func (v Validator) GetMinSelfDelegation() math.Int { return sdk.ZeroInt() } func (v Validator) GetDelegatorShares() math.LegacyDec { return v.DelegatorShares } +func (v Validator) GetLiquidShares() sdk.Dec { return v.LiquidShares } // UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces func (v Validator) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error {