diff --git a/x/staking/keeper/liquid_stake.go b/x/staking/keeper/liquid_stake.go index acde931f50c2..a0e9df1c974b 100644 --- a/x/staking/keeper/liquid_stake.go +++ b/x/staking/keeper/liquid_stake.go @@ -164,6 +164,13 @@ func (k Keeper) DecreaseValidatorLiquidShares(ctx sdk.Context, validator types.V return 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, validator types.Validator, shares sdk.Dec) { + validator.ValidatorBondShares = validator.ValidatorBondShares.Add(shares) + k.SetValidator(ctx, validator) +} + // 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 diff --git a/x/staking/keeper/msg_server.go b/x/staking/keeper/msg_server.go index 8e9e4279a634..ffdf1bcf6019 100644 --- a/x/staking/keeper/msg_server.go +++ b/x/staking/keeper/msg_server.go @@ -232,6 +232,15 @@ func (k msgServer) Delegate(goCtx context.Context, msg *types.MsgDelegate) (*typ 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 { + k.IncreaseValidatorBondShares(ctx, validator, newShares) + } + if tokens.IsInt64() { defer func() { telemetry.IncrCounter(1, types.ModuleName, "delegate") @@ -300,12 +309,9 @@ func (k msgServer) BeginRedelegate(goCtx context.Context, msg *types.MsgBeginRed if err != nil { return nil, err } - dstShares, err := dstValidator.SharesFromTokensTruncated(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 + // 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 delegation.ValidatorBond { if err := k.SafelyDecreaseValidatorBond(ctx, srcValidator, srcShares); err != nil { return nil, err @@ -322,6 +328,10 @@ func (k msgServer) BeginRedelegate(goCtx context.Context, msg *types.MsgBeginRed // 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, dstValidator, dstShares); err != nil { return nil, err } @@ -547,11 +557,11 @@ 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 - shares, err := validator.SharesFromTokens(tokens) - if err != nil { - return nil, err - } 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 } diff --git a/x/staking/keeper/msg_server_test.go b/x/staking/keeper/msg_server_test.go index 545365ee5cff..c838871dcf3e 100644 --- a/x/staking/keeper/msg_server_test.go +++ b/x/staking/keeper/msg_server_test.go @@ -993,6 +993,106 @@ func TestValidatorBond(t *testing.T) { } } +func TestChangeValidatorBond(t *testing.T) { + _, app, ctx := createTestInput() + msgServer := keeper.NewMsgServerImpl(app.StakingKeeper) + + checkValidatorBondShares := func(validatorAddress sdk.ValAddress, expectedShares sdk.Int) { + validator, found := app.StakingKeeper.GetValidator(ctx, validatorAddress) + require.True(t, found, "validator should have been found") + require.Equal(t, expectedShares.Int64(), validator.ValidatorBondShares.TruncateInt64(), "validator bond shares") + } + + // Create a delegator and 2 validators + addresses := simapp.AddTestAddrs(app, ctx, 3, sdk.NewInt(1_000_000)) + pubKeys := simapp.CreateTestPubKeys(3) + + validatorAPubKey := pubKeys[1] + validatorBPubKey := pubKeys[2] + + delegatorAddress := addresses[0] + validatorAAddress := sdk.ValAddress(validatorAPubKey.Address()) + validatorBAddress := sdk.ValAddress(validatorBPubKey.Address()) + + validatorA := teststaking.NewValidator(t, validatorAAddress, validatorAPubKey) + validatorB := teststaking.NewValidator(t, validatorBAddress, validatorBPubKey) + + validatorA.Tokens = sdk.NewInt(1_000_000) + validatorB.Tokens = sdk.NewInt(1_000_000) + validatorA.DelegatorShares = sdk.NewDec(1_000_000) + validatorB.DelegatorShares = sdk.NewDec(1_000_000) + + app.StakingKeeper.SetValidator(ctx, validatorA) + app.StakingKeeper.SetValidator(ctx, validatorB) + + // The test will go through Delegate/Redelegate/Undelegate messages with the following + delegation1Amount := sdk.NewInt(1000) + delegation2Amount := sdk.NewInt(1000) + redelegateAmount := sdk.NewInt(500) + undelegateAmount := sdk.NewInt(500) + + delegate1Coin := sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), delegation1Amount) + delegate2Coin := sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), delegation2Amount) + redelegateCoin := sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), redelegateAmount) + undelegateCoin := sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), undelegateAmount) + + // Delegate to validator A - validator bond shares should not change + _, err := msgServer.Delegate(sdk.WrapSDKContext(ctx), &types.MsgDelegate{ + DelegatorAddress: delegatorAddress.String(), + ValidatorAddress: validatorAAddress.String(), + Amount: delegate1Coin, + }) + require.NoError(t, err, "no error expected during first delegation") + + checkValidatorBondShares(validatorAAddress, sdk.ZeroInt()) + checkValidatorBondShares(validatorBAddress, sdk.ZeroInt()) + + // Flag the delegation as a validator bond + _, err = msgServer.ValidatorBond(sdk.WrapSDKContext(ctx), &types.MsgValidatorBond{ + DelegatorAddress: delegatorAddress.String(), + ValidatorAddress: validatorAAddress.String(), + }) + require.NoError(t, err, "no error expected during validator bond") + + checkValidatorBondShares(validatorAAddress, delegation1Amount) + checkValidatorBondShares(validatorBAddress, sdk.ZeroInt()) + + // Delegate more - it should increase the validator bond shares + _, err = msgServer.Delegate(sdk.WrapSDKContext(ctx), &types.MsgDelegate{ + DelegatorAddress: delegatorAddress.String(), + ValidatorAddress: validatorAAddress.String(), + Amount: delegate2Coin, + }) + require.NoError(t, err, "no error expected during second delegation") + + checkValidatorBondShares(validatorAAddress, delegation1Amount.Add(delegation2Amount)) + checkValidatorBondShares(validatorBAddress, sdk.ZeroInt()) + + // Redelegate partially from A to B - it should remove the bond shares from the source validator + _, err = msgServer.BeginRedelegate(sdk.WrapSDKContext(ctx), &types.MsgBeginRedelegate{ + DelegatorAddress: delegatorAddress.String(), + ValidatorSrcAddress: validatorAAddress.String(), + ValidatorDstAddress: validatorBAddress.String(), + Amount: redelegateCoin, + }) + require.NoError(t, err, "no error expected during redelegation") + + checkValidatorBondShares(validatorAAddress, delegation1Amount.Add(delegation2Amount).Sub(redelegateAmount)) + checkValidatorBondShares(validatorBAddress, sdk.ZeroInt()) + + // Undelegate from validator A - it should have removed the shares + _, err = msgServer.Undelegate(sdk.WrapSDKContext(ctx), &types.MsgUndelegate{ + DelegatorAddress: delegatorAddress.String(), + ValidatorAddress: validatorAAddress.String(), + Amount: undelegateCoin, + }) + require.NoError(t, err, "no error expected during undelegation") + + expectedBondShares := delegation1Amount.Add(delegation2Amount).Sub(redelegateAmount).Sub(undelegateAmount) + checkValidatorBondShares(validatorAAddress, expectedBondShares) + checkValidatorBondShares(validatorBAddress, sdk.ZeroInt()) +} + func TestEnableDisableTokenizeShares(t *testing.T) { _, app, ctx := createTestInput() msgServer := keeper.NewMsgServerImpl(app.StakingKeeper)