Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add validator cap and bond checks when creating the delegation strategy #810

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Ref: https://keepachangelog.com/en/1.0.0/

### Bug Fixes

- [810](https://github.com/persistenceOne/pstake-native/pull/810) Add validator cap and bond checks when creating the delegation strategy
- [792](https://github.com/persistenceOne/pstake-native/pull/792) Use GetHostChainFromHostDenom in ICA Transfer
unsuccessfulAck instead of GetHostChainFromDelegatorAddress as Rewards account too uses ICA Transfer to autocompound
- [795](https://github.com/persistenceOne/pstake-native/pull/795) Reject zero weight validator LSM shares for
Expand Down
2 changes: 1 addition & 1 deletion x/liquidstake/keeper/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func (s *KeeperTestSuite) TestImportExportGenesis() {
k.SetParams(ctx, params)
k.UpdateLiquidValidatorSet(ctx)

stakingAmt := math.NewInt(100000000)
stakingAmt := math.NewInt(100000)
s.Require().NoError(s.liquidStaking(s.delAddrs[0], stakingAmt))
lvs := k.GetAllLiquidValidators(ctx)
s.Require().Len(lvs, 2)
Expand Down
6 changes: 5 additions & 1 deletion x/liquidstake/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,11 @@ func (s *KeeperTestSuite) CreateValidators(powers []int64) ([]sdk.AccAddress, []
valAddrs := testhelpers.ConvertAddrsToValAddrs(addrs)
pks := testhelpers.CreateTestPubKeys(num)
skParams := s.app.StakingKeeper.GetParams(s.ctx)
skParams.ValidatorLiquidStakingCap = sdk.OneDec()
globalCap, _ := sdk.NewDecFromStr("0.1")
skParams.GlobalLiquidStakingCap = globalCap
validatorCap, _ := sdk.NewDecFromStr("0.5")
skParams.ValidatorLiquidStakingCap = validatorCap
skParams.ValidatorBondFactor = sdk.NewDec(250)
s.app.StakingKeeper.SetParams(s.ctx, skParams)
for i, power := range powers {
val, err := stakingtypes.NewValidator(valAddrs[i], pks[i], stakingtypes.Description{})
Expand Down
102 changes: 91 additions & 11 deletions x/liquidstake/keeper/liquidstake.go
Original file line number Diff line number Diff line change
Expand Up @@ -547,23 +547,27 @@ func (k Keeper) LSMDelegate(
}

// LiquidDelegate delegates staking amount to active validators by proxy account.
func (k Keeper) LiquidDelegate(ctx sdk.Context, proxyAcc sdk.AccAddress, activeVals types.ActiveLiquidValidators, stakingAmt math.Int, whitelistedValsMap types.WhitelistedValsMap) (err error) {
// crumb may occur due to a decimal point error in dividing the staking amount into the weight of liquid validators, It added on first active liquid validator
weightedAmt, crumb := types.DivideByWeight(activeVals, stakingAmt, whitelistedValsMap)
if len(weightedAmt) == 0 {
func (k Keeper) LiquidDelegate(
ctx sdk.Context,
proxyAcc sdk.AccAddress,
activeVals types.ActiveLiquidValidators,
stakingAmt math.Int,
whitelistedValsMap types.WhitelistedValsMap,
) (err error) {
delegations := k.DivideByWeight(ctx, activeVals, stakingAmt, whitelistedValsMap)
if len(delegations) == 0 {
return types.ErrInvalidActiveLiquidValidators
}
weightedAmt[0] = weightedAmt[0].Add(crumb)
for i, val := range activeVals {
if !weightedAmt[i].IsPositive() {
continue
}
validator, _ := k.stakingKeeper.GetValidator(ctx, val.GetOperator())
err = k.DelegateWithCap(ctx, proxyAcc, validator, weightedAmt[i])

for valStrAddr, delegationAmt := range delegations {
valAddr, _ := sdk.ValAddressFromBech32(valStrAddr)
validator, _ := k.stakingKeeper.GetValidator(ctx, valAddr)
err = k.DelegateWithCap(ctx, proxyAcc, validator, delegationAmt)
if err != nil {
return err
}
}

return nil
}

Expand Down Expand Up @@ -719,6 +723,82 @@ func (k Keeper) LiquidUnbond(
return completionTime, returnAmount, ubd, nil
}

// DivideByWeight divide the input amount to delegate between the active validators, specifically checking if they have
// room to receive more liquid delegations. The leftover that might be left is divided across all validators that can
// receive it.
func (k Keeper) DivideByWeight(
kruspy marked this conversation as resolved.
Show resolved Hide resolved
ctx sdk.Context,
avs types.ActiveLiquidValidators,
totalInput math.Int,
whitelistedValsMap types.WhitelistedValsMap,
) map[string]math.Int {
totalWeight := avs.TotalWeight(whitelistedValsMap)
if !totalWeight.IsPositive() {
return map[string]math.Int{}
}

totalDelegation := sdk.ZeroInt()
delegations := make(map[string]math.Int)
inputPerValidator := math.LegacyNewDecFromInt(totalInput).QuoTruncate(math.LegacyNewDecFromInt(totalWeight))

// loop through all validators and check if the amount of shares they would receive is within the validator specific caps
for _, val := range avs {
validator, _ := k.stakingKeeper.GetValidator(ctx, val.GetOperator())

// calculate the shares the input would receive
delegation := inputPerValidator.MulInt(val.GetWeight(whitelistedValsMap, true)).TruncateInt()
if !delegation.IsPositive() { // continue if the delegation is 0
continue
}
delegationShares := validator.GetDelegatorShares().MulInt(delegation).QuoInt(validator.GetTokens())

// just delegate if the validator does not exceed any of the validator specific caps
if !k.stakingKeeper.CheckExceedsValidatorBondCap(ctx, validator, delegationShares) &&
!k.stakingKeeper.CheckExceedsValidatorLiquidStakingCap(ctx, validator, delegationShares, false) {
totalDelegation = totalDelegation.Add(delegation)
delegations[val.GetOperator().String()] = delegation
}
}

if len(delegations) == 0 {
return map[string]math.Int{}
}

// the leftover amount that could not be delegated is divided across all validators
leftOver := totalInput.Sub(totalDelegation)
numValidators := math.NewInt(int64(len(delegations)))
for leftOver.IsPositive() {
diffPerValidator := leftOver.Quo(numValidators)
if leftOver.LT(numValidators) { // if the leftover is less than the number of validators to be delegated, just add it to the first one that can receive it
diffPerValidator = leftOver
}
for strAddr := range delegations {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// get the new delegation amount by adding the leftover to the existing delegation
newDelegationAmount := delegations[strAddr].Add(diffPerValidator)

// calculate the shares the input would receive
valAddr, _ := sdk.ValAddressFromBech32(strAddr)
validator, _ := k.stakingKeeper.GetValidator(ctx, valAddr)
delegationShares := validator.GetDelegatorShares().MulInt(newDelegationAmount).QuoInt(validator.GetTokens())

// if the validator can receive the leftover amount, update the map entry and add it to the total delegated amount
if !k.stakingKeeper.CheckExceedsValidatorBondCap(ctx, validator, delegationShares) &&
!k.stakingKeeper.CheckExceedsValidatorLiquidStakingCap(ctx, validator, delegationShares, false) {
totalDelegation = totalDelegation.Add(diffPerValidator)
delegations[strAddr] = newDelegationAmount
}

// recalculate the leftover amount after each pass
leftOver = totalInput.Sub(totalDelegation)
if leftOver.IsZero() {
break
}
}
}

return delegations
}

// PrioritiseInactiveLiquidValidators sorts LiquidValidators array to have inactive validators first. Used for the case when
// unbonding should begin from the inactive validators first.
func (k Keeper) PrioritiseInactiveLiquidValidators(
Expand Down
189 changes: 178 additions & 11 deletions x/liquidstake/keeper/liquidstake_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package keeper_test

import (
"testing"
"time"

"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/stretchr/testify/require"

testhelpers "github.com/persistenceOne/pstake-native/v2/app/helpers"
"github.com/persistenceOne/pstake-native/v2/x/liquidstake/types"
Expand Down Expand Up @@ -98,11 +100,7 @@ func (s *KeeperTestSuite) TestLiquidStake() {
s.ctx, types.LiquidStakeProxyAcc, valOpers[2],
)
s.Require().True(found)
s.Require().Equal(proxyAccDel1.Shares, math.LegacyNewDec(16668))
s.Require().Equal(proxyAccDel2.Shares, math.LegacyNewDec(16666))
s.Require().Equal(proxyAccDel2.Shares, math.LegacyNewDec(16666))
s.Require().Equal(stakingAmt.ToLegacyDec(),
proxyAccDel1.Shares.Add(proxyAccDel2.Shares).Add(proxyAccDel3.Shares))
s.Require().Equal(stakingAmt.ToLegacyDec(), proxyAccDel1.Shares.Add(proxyAccDel2.Shares).Add(proxyAccDel3.Shares))

liquidBondDenom := s.keeper.LiquidBondDenom(s.ctx)
balanceBeforeUBD := s.app.BankKeeper.GetBalance(
Expand Down Expand Up @@ -181,9 +179,7 @@ func (s *KeeperTestSuite) TestLiquidStake() {
s.ctx, types.LiquidStakeProxyAcc, valOpers[2],
)
s.Require().True(found)
s.Require().Equal(math.LegacyNewDec(13335), proxyAccDel1.Shares)
s.Require().Equal(math.LegacyNewDec(13333), proxyAccDel2.Shares)
s.Require().Equal(math.LegacyNewDec(13333), proxyAccDel3.Shares)
s.Require().Equal(stakingAmt.Sub(unbondingAmt).ToLegacyDec(), proxyAccDel1.Shares.Add(proxyAccDel2.Shares).Add(proxyAccDel3.Shares))

res = s.keeper.GetAllLiquidValidatorStates(s.ctx)
s.Require().Equal(params.WhitelistedValidators[0].ValidatorAddress,
Expand Down Expand Up @@ -243,7 +239,7 @@ func (s *KeeperTestSuite) TestLiquidStake() {
}

func (s *KeeperTestSuite) TestLiquidStakeFromVestingAccount() {
_, valOpers, _ := s.CreateValidators([]int64{1000000, 2000000, 3000000})
_, valOpers, _ := s.CreateValidators([]int64{1000000000, 2000000000, 3000000000})
params := s.keeper.GetParams(s.ctx)

// add active validator
Expand Down Expand Up @@ -312,6 +308,11 @@ func (s *KeeperTestSuite) TestLiquidStakeEdgeCases() {
s.Require().ErrorIs(err, types.ErrInvalidBondDenom)

// liquid stake, unstaking with huge amount
stakingParams := s.app.StakingKeeper.GetParams(s.ctx)
stakingParams.GlobalLiquidStakingCap = sdk.OneDec()
stakingParams.ValidatorLiquidStakingCap = sdk.OneDec()
stakingParams.ValidatorBondFactor = sdk.NewDec(10000000000000)
s.app.StakingKeeper.SetParams(s.ctx, stakingParams)
hugeAmt := math.NewInt(1_000_000_000_000_000_000)
s.fundAddr(s.delAddrs[0], sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, hugeAmt.MulRaw(2))))
s.Require().NoError(s.liquidStaking(s.delAddrs[0], hugeAmt))
Expand All @@ -334,7 +335,7 @@ func (s *KeeperTestSuite) TestLiquidUnstakeEdgeCases() {
_, valOpers, _ := s.CreateValidators([]int64{1000000, 2000000, 3000000})
params := s.keeper.GetParams(s.ctx)
s.keeper.UpdateLiquidValidatorSet(s.ctx)
stakingAmt := math.NewInt(5000000)
stakingAmt := math.NewInt(100000)

// add active validator
params.WhitelistedValidators = []types.WhitelistedValidator{
Expand Down Expand Up @@ -415,7 +416,7 @@ func (s *KeeperTestSuite) TestShareInflation() {
s.keeper.SetParams(s.ctx, params)
s.keeper.UpdateLiquidValidatorSet(s.ctx)

initialStakingAmt := math.NewInt(1) // little amount
initialStakingAmt := math.NewInt(10) // little amount
initializingStakingAmt := math.NewInt(10000) // normal amount
attacker := s.delAddrs[0]
user := s.delAddrs[1]
Expand Down Expand Up @@ -458,3 +459,169 @@ func (s *KeeperTestSuite) TestShareInflation() {
attackerProfit := unbondingAmt.Sub(initialStakingAmt).Sub(attackerTransferAmount)
s.Require().LessOrEqual(attackerProfit.Int64(), math.ZeroInt().Int64())
}

func (s *KeeperTestSuite) TestDivideByWeight() {
_, valOpers, _ := s.CreateValidators([]int64{2000000, 2000000, 2000000})

testCases := []struct {
name string
whitelistedVals []types.WhitelistedValidator
addStakingAmt math.Int
expectedDelegations map[string]math.Int
}{
{
name: "Success with leftover less than delegations length",
whitelistedVals: []types.WhitelistedValidator{
{
ValidatorAddress: valOpers[0].String(),
TargetWeight: math.NewInt(1),
},
{
ValidatorAddress: valOpers[1].String(),
TargetWeight: math.NewInt(1),
},
{
ValidatorAddress: valOpers[2].String(),
TargetWeight: math.NewInt(1),
},
},
addStakingAmt: math.NewInt(100000),
expectedDelegations: map[string]math.Int{
valOpers[0].String(): math.NewInt(33334),
valOpers[1].String(): math.NewInt(33333),
valOpers[2].String(): math.NewInt(33333),
},
},
{
name: "Success without leftover",
whitelistedVals: []types.WhitelistedValidator{
{
ValidatorAddress: valOpers[0].String(),
TargetWeight: math.NewInt(2),
},
{
ValidatorAddress: valOpers[1].String(),
TargetWeight: math.NewInt(2),
},
{
ValidatorAddress: valOpers[2].String(),
TargetWeight: math.NewInt(1),
},
},
addStakingAmt: math.NewInt(100000),
expectedDelegations: map[string]math.Int{
valOpers[0].String(): math.NewInt(40000),
valOpers[1].String(): math.NewInt(40000),
valOpers[2].String(): math.NewInt(20000),
},
},
{
name: "First validator reaches the cap, the leftover gets divided among validators",
whitelistedVals: []types.WhitelistedValidator{
{
ValidatorAddress: valOpers[0].String(),
TargetWeight: math.NewInt(8),
},
{
ValidatorAddress: valOpers[1].String(),
TargetWeight: math.NewInt(1),
},
{
ValidatorAddress: valOpers[2].String(),
TargetWeight: math.NewInt(1),
},
},
addStakingAmt: math.NewInt(2500003),
expectedDelegations: map[string]math.Int{
valOpers[1].String(): math.NewInt(1250002),
valOpers[2].String(): math.NewInt(1250001),
},
},
{
name: "First validator reaches the cap, the leftover gets divided among validators evenly",
whitelistedVals: []types.WhitelistedValidator{
{
ValidatorAddress: valOpers[0].String(),
TargetWeight: math.NewInt(8),
},
{
ValidatorAddress: valOpers[1].String(),
TargetWeight: math.NewInt(1),
},
{
ValidatorAddress: valOpers[2].String(),
TargetWeight: math.NewInt(1),
},
},
addStakingAmt: math.NewInt(2500002),
expectedDelegations: map[string]math.Int{
valOpers[1].String(): math.NewInt(1250001),
valOpers[2].String(): math.NewInt(1250001),
},
},
{
name: "All validators reach the cap",
whitelistedVals: []types.WhitelistedValidator{
{
ValidatorAddress: valOpers[0].String(),
TargetWeight: math.NewInt(1),
},
{
ValidatorAddress: valOpers[1].String(),
TargetWeight: math.NewInt(1),
},
{
ValidatorAddress: valOpers[2].String(),
TargetWeight: math.NewInt(1),
},
},
addStakingAmt: math.NewInt(1000000000),
expectedDelegations: map[string]math.Int{},
},
{
name: "Amount below minimum",
whitelistedVals: []types.WhitelistedValidator{
{
ValidatorAddress: valOpers[0].String(),
TargetWeight: math.NewInt(1),
},
{
ValidatorAddress: valOpers[1].String(),
TargetWeight: math.NewInt(1),
},
{
ValidatorAddress: valOpers[2].String(),
TargetWeight: math.NewInt(1),
},
},
addStakingAmt: math.NewInt(1),
expectedDelegations: map[string]math.Int{},
},
}

for _, tc := range testCases {
s.T().Run(tc.name, func(t *testing.T) {
require.IsType(t, []types.WhitelistedValidator{}, tc.whitelistedVals)
require.IsType(t, math.Int{}, tc.addStakingAmt)
require.IsType(t, map[string]math.Int{}, tc.expectedDelegations)

valsMap := types.GetWhitelistedValsMap(tc.whitelistedVals)
var activeVals types.ActiveLiquidValidators
for _, v := range tc.whitelistedVals {
activeVals = append(activeVals, types.LiquidValidator{
OperatorAddress: v.ValidatorAddress,
})
}
delegations := s.keeper.DivideByWeight(s.ctx, activeVals, tc.addStakingAmt, valsMap)

require.EqualValues(t, tc.expectedDelegations, delegations)
totalDelegationAmount := sdk.ZeroInt()
for _, d := range delegations {
totalDelegationAmount = totalDelegationAmount.Add(d)
}
if !(len(delegations) == 0) {
require.EqualValues(t, tc.addStakingAmt, totalDelegationAmount)
}
})
}
}
Loading
Loading