From 197fcf02f4e27d086e5cc35a1ab9fda0165f1c2c Mon Sep 17 00:00:00 2001 From: Philip Offtermatt Date: Wed, 22 May 2024 12:49:08 +0200 Subject: [PATCH 1/7] Add testcase for disabled redelegation --- .../staking/keeper/msg_server_test.go | 95 ++++++++++++++++++- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/tests/integration/staking/keeper/msg_server_test.go b/tests/integration/staking/keeper/msg_server_test.go index 4c7336c2f52c..76f7962a2156 100644 --- a/tests/integration/staking/keeper/msg_server_test.go +++ b/tests/integration/staking/keeper/msg_server_test.go @@ -19,6 +19,7 @@ import ( bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" banktestutil "github.com/cosmos/cosmos-sdk/x/bank/testutil" minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + "github.com/cosmos/cosmos-sdk/x/staking" "github.com/cosmos/cosmos-sdk/x/staking/keeper" "github.com/cosmos/cosmos-sdk/x/staking/testutil" "github.com/cosmos/cosmos-sdk/x/staking/types" @@ -161,9 +162,7 @@ func TestCancelUnbondingDelegation(t *testing.T) { func TestTokenizeSharesAndRedeemTokens(t *testing.T) { _, app, ctx := createTestInput(t) - var ( - stakingKeeper = app.StakingKeeper - ) + stakingKeeper := app.StakingKeeper liquidStakingCapStrict := sdk.ZeroDec() liquidStakingCapConservative := sdk.MustNewDecFromStr("0.8") @@ -1712,3 +1711,93 @@ func createICAAccount(ctx sdk.Context, ak accountkeeper.AccountKeeper) sdk.AccAd return icaAddress } + +func TestRedelegationTokenization(t *testing.T) { + // Test that a delegator with ongoing redelegation cannot + // tokenize any shares until the redelegation is complete. + _, app, ctx := createTestInput(t) + var ( + stakingKeeper = app.StakingKeeper + bankKeeper = app.BankKeeper + ) + msgServer := keeper.NewMsgServerImpl(stakingKeeper) + validatorA := stakingKeeper.GetAllValidators(ctx)[0] + validatorAAddress := validatorA.GetOperator() + _, validatorBAddress := setupTestTokenizeAndRedeemConversion(t, *stakingKeeper, bankKeeper, ctx) + + addrs := simtestutil.AddTestAddrs(bankKeeper, stakingKeeper, ctx, 2, stakingKeeper.TokensFromConsensusPower(ctx, 10000)) + alice := addrs[0] + + delegateAmount := sdk.TokensFromConsensusPower(10, sdk.DefaultPowerReduction) + delegateCoin := sdk.NewCoin(stakingKeeper.BondDenom(ctx), delegateAmount) + + // Alice delegates to validatorA + _, err := msgServer.Delegate(sdk.WrapSDKContext(ctx), &types.MsgDelegate{ + DelegatorAddress: alice.String(), + ValidatorAddress: validatorAAddress.String(), + Amount: delegateCoin, + }) + + // Alice redelegates to validatorB + redelegateAmount := sdk.TokensFromConsensusPower(5, sdk.DefaultPowerReduction) + redelegateCoin := sdk.NewCoin(stakingKeeper.BondDenom(ctx), redelegateAmount) + _, err = msgServer.BeginRedelegate(sdk.WrapSDKContext(ctx), &types.MsgBeginRedelegate{ + DelegatorAddress: alice.String(), + ValidatorSrcAddress: validatorAAddress.String(), + ValidatorDstAddress: validatorBAddress.String(), + Amount: redelegateCoin, + }) + require.NoError(t, err) + + redelegation := stakingKeeper.GetRedelegations(ctx, alice, uint16(10)) + require.Len(t, redelegation, 1, "expect one redelegation") + require.Len(t, redelegation[0].Entries, 1, "expect one redelegation entry") + + // Alice attempts to tokenize the redelegation, but this fails because the redelegation is ongoing + tokenizedAmount := sdk.TokensFromConsensusPower(5, sdk.DefaultPowerReduction) + tokenizedCoin := sdk.NewCoin(stakingKeeper.BondDenom(ctx), tokenizedAmount) + _, err = msgServer.TokenizeShares(sdk.WrapSDKContext(ctx), &types.MsgTokenizeShares{ + DelegatorAddress: alice.String(), + ValidatorAddress: validatorBAddress.String(), + Amount: tokenizedCoin, + TokenizedShareOwner: alice.String(), + }) + require.Error(t, err) + require.Equal(t, types.ErrRedelegationInProgress, err) + + // Check that the redelegation is still present + redelegation = stakingKeeper.GetRedelegations(ctx, alice, uint16(10)) + require.Len(t, redelegation, 1, "expect one redelegation") + require.Len(t, redelegation[0].Entries, 1, "expect one redelegation entry") + + // advance time until the redelegations should mature + // end block + staking.EndBlocker(ctx, stakingKeeper) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + // advance by 22 days + ctx = ctx.WithBlockTime(ctx.BlockTime().Add(22 * 24 * time.Hour)) + // begin block + staking.BeginBlocker(ctx, stakingKeeper) + // end block + staking.EndBlocker(ctx, stakingKeeper) + + // check that there the redelegation is removed + redelegation = stakingKeeper.GetRedelegations(ctx, alice, uint16(10)) + require.Len(t, redelegation, 0, "expect no redelegations") + + // Alice attempts to tokenize the redelegation again, and this time it should succeed + // because there is no ongoing redelegation + _, err = msgServer.TokenizeShares(sdk.WrapSDKContext(ctx), &types.MsgTokenizeShares{ + DelegatorAddress: alice.String(), + ValidatorAddress: validatorBAddress.String(), + Amount: tokenizedCoin, + TokenizedShareOwner: alice.String(), + }) + require.NoError(t, err) + + // Check that the tokenization was successful + shareRecord, err := stakingKeeper.GetTokenizeShareRecord(ctx, stakingKeeper.GetLastTokenizeShareRecordID(ctx)) + require.NoError(t, err, "expect to find token share record") + require.Equal(t, alice.String(), shareRecord.Owner) + require.Equal(t, validatorBAddress.String(), shareRecord.Validator) +} From cd9a32c66babc4afee0333422773885f8e1e9f86 Mon Sep 17 00:00:00 2001 From: Philip Offtermatt Date: Wed, 22 May 2024 12:49:25 +0200 Subject: [PATCH 2/7] Fix typo --- tests/integration/staking/keeper/msg_server_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/staking/keeper/msg_server_test.go b/tests/integration/staking/keeper/msg_server_test.go index 76f7962a2156..5a622ab1b749 100644 --- a/tests/integration/staking/keeper/msg_server_test.go +++ b/tests/integration/staking/keeper/msg_server_test.go @@ -1781,7 +1781,7 @@ func TestRedelegationTokenization(t *testing.T) { // end block staking.EndBlocker(ctx, stakingKeeper) - // check that there the redelegation is removed + // check that the redelegation is removed redelegation = stakingKeeper.GetRedelegations(ctx, alice, uint16(10)) require.Len(t, redelegation, 0, "expect no redelegations") From 9e343dbd224ed41bd3645444e511fe21085cb705 Mon Sep 17 00:00:00 2001 From: Simon Noetzlin Date: Wed, 22 May 2024 16:50:36 +0200 Subject: [PATCH 3/7] disable tokenization of shares being redelegated --- x/staking/keeper/msg_server.go | 5 +++++ x/staking/types/errors.go | 1 + 2 files changed, 6 insertions(+) diff --git a/x/staking/keeper/msg_server.go b/x/staking/keeper/msg_server.go index a88af57ec750..67c31f0a8e08 100644 --- a/x/staking/keeper/msg_server.go +++ b/x/staking/keeper/msg_server.go @@ -716,6 +716,11 @@ func (k msgServer) TokenizeShares(goCtx context.Context, msg *types.MsgTokenizeS return nil, err } + // Check that the delegator doesn't have an ongoing redelegation to the validator + if k.HasReceivingRedelegation(ctx, delegatorAddress, valAddr) { + return nil, types.ErrRedelegationInProgress + } + // 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, diff --git a/x/staking/types/errors.go b/x/staking/types/errors.go index a684f5898af3..338b5e147066 100644 --- a/x/staking/types/errors.go +++ b/x/staking/types/errors.go @@ -71,4 +71,5 @@ var ( ErrValidatorLiquidSharesUnderflow = sdkerrors.Register(ModuleName, 117, "validator liquid shares underflow") ErrTotalLiquidStakedUnderflow = sdkerrors.Register(ModuleName, 118, "total liquid staked underflow") ErrTinyRedemptionAmount = sdkerrors.Register(ModuleName, 119, "too few tokens to redeem (truncates to zero tokens)") + ErrRedelegationInProgress = sdkerrors.Register(ModuleName, 120, "delegator is not allowed to tokenize shares from validator with ongoing redelegation") ) From 02358af367820e64113da40c890d1205e1dcf8c8 Mon Sep 17 00:00:00 2001 From: Simon Noetzlin Date: Wed, 22 May 2024 17:05:22 +0200 Subject: [PATCH 4/7] nit --- x/staking/types/errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/staking/types/errors.go b/x/staking/types/errors.go index 338b5e147066..7cb6d7c2777b 100644 --- a/x/staking/types/errors.go +++ b/x/staking/types/errors.go @@ -71,5 +71,5 @@ var ( ErrValidatorLiquidSharesUnderflow = sdkerrors.Register(ModuleName, 117, "validator liquid shares underflow") ErrTotalLiquidStakedUnderflow = sdkerrors.Register(ModuleName, 118, "total liquid staked underflow") ErrTinyRedemptionAmount = sdkerrors.Register(ModuleName, 119, "too few tokens to redeem (truncates to zero tokens)") - ErrRedelegationInProgress = sdkerrors.Register(ModuleName, 120, "delegator is not allowed to tokenize shares from validator with ongoing redelegation") + ErrRedelegationInProgress = sdkerrors.Register(ModuleName, 120, "delegator is not allowed to tokenize shares from validator with a redelegation in progress") ) From 06eb2e5d4ef0f2c2cd48628a6938c34cb8b510fa Mon Sep 17 00:00:00 2001 From: Simon Noetzlin Date: Fri, 24 May 2024 10:15:15 +0200 Subject: [PATCH 5/7] add changelog entry --- CHANGELOG.md | 6 ++++++ x/staking/keeper/msg_server.go | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 569fa477947f..95e5c043325f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,12 @@ Ref: https://keepachangelog.com/en/1.0.0/ # Changelog +## [Unreleased] + +### Improvements + +* (staking) [#20444](https://github.com/cosmos/cosmos-sdk/pull/20444) Disable tokenization of shares from redelegations. + ## v0.47.11-ics-lsm This is a special cosmos-sdk release with support for both ICS and LSM. diff --git a/x/staking/keeper/msg_server.go b/x/staking/keeper/msg_server.go index 67c31f0a8e08..6bc69a000877 100644 --- a/x/staking/keeper/msg_server.go +++ b/x/staking/keeper/msg_server.go @@ -716,7 +716,11 @@ func (k msgServer) TokenizeShares(goCtx context.Context, msg *types.MsgTokenizeS return nil, err } +<<<<<<< HEAD // Check that the delegator doesn't have an ongoing redelegation to the validator +======= + // Check that the delegator has no ongoing redelegation to the validator +>>>>>>> 37001ff9f (add changelog entry) if k.HasReceivingRedelegation(ctx, delegatorAddress, valAddr) { return nil, types.ErrRedelegationInProgress } From 1060822a6637f3bdb3b723cb448088d7f77c31ce Mon Sep 17 00:00:00 2001 From: Simon Noetzlin Date: Fri, 24 May 2024 10:30:22 +0200 Subject: [PATCH 6/7] fix conflicts --- x/staking/keeper/msg_server.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/x/staking/keeper/msg_server.go b/x/staking/keeper/msg_server.go index 6bc69a000877..244ca25c8d85 100644 --- a/x/staking/keeper/msg_server.go +++ b/x/staking/keeper/msg_server.go @@ -716,11 +716,7 @@ func (k msgServer) TokenizeShares(goCtx context.Context, msg *types.MsgTokenizeS return nil, err } -<<<<<<< HEAD - // Check that the delegator doesn't have an ongoing redelegation to the validator -======= - // Check that the delegator has no ongoing redelegation to the validator ->>>>>>> 37001ff9f (add changelog entry) + // Check that the delegator has no ongoing redelegations to the validator if k.HasReceivingRedelegation(ctx, delegatorAddress, valAddr) { return nil, types.ErrRedelegationInProgress } From 27461f635a1e0f3ab78adae5aad864867d8e5816 Mon Sep 17 00:00:00 2001 From: Simon Noetzlin Date: Fri, 24 May 2024 10:59:52 +0200 Subject: [PATCH 7/7] fix non-deterministic tests --- x/staking/simulation/operations.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x/staking/simulation/operations.go b/x/staking/simulation/operations.go index 6522b70450cf..febaa8962643 100644 --- a/x/staking/simulation/operations.go +++ b/x/staking/simulation/operations.go @@ -773,6 +773,11 @@ func SimulateMsgTokenizeShares(ak types.AccountKeeper, bk types.BankKeeper, k *k return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgTokenizeShares, "tokenize shares disabled"), nil, nil } + // Make sure that the delegator has no ongoing redelegations to the validator + if k.HasReceivingRedelegation(ctx, delAddr, srcAddr) { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgTokenizeShares, "delegator has redelegations in progress"), nil, nil + } + // get random destination validator totalBond := validator.TokensFromShares(delegation.GetShares()).TruncateInt() if !totalBond.IsPositive() {